diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..e9225c92b08 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.p[lm]] +indent_style = tab +indent_size = 8 diff --git a/Bin/MSWin32-x86-multi-thread/cyggomp-1.dll b/Bin/MSWin32-x86-multi-thread/cyggomp-1.dll deleted file mode 100755 index e9e09472db4..00000000000 Binary files a/Bin/MSWin32-x86-multi-thread/cyggomp-1.dll and /dev/null differ diff --git a/Bin/MSWin32-x86-multi-thread/cygwin1.dll b/Bin/MSWin32-x86-multi-thread/cygwin1.dll deleted file mode 100755 index 0296b95d7fb..00000000000 Binary files a/Bin/MSWin32-x86-multi-thread/cygwin1.dll and /dev/null differ diff --git a/Bin/MSWin32-x86-multi-thread/faad.exe b/Bin/MSWin32-x86-multi-thread/faad.exe index b98969c6727..b40592b3aca 100755 Binary files a/Bin/MSWin32-x86-multi-thread/faad.exe and b/Bin/MSWin32-x86-multi-thread/faad.exe differ diff --git a/Bin/MSWin32-x86-multi-thread/flac.exe b/Bin/MSWin32-x86-multi-thread/flac.exe index 3946ba2067c..5b628fc74af 100755 Binary files a/Bin/MSWin32-x86-multi-thread/flac.exe and b/Bin/MSWin32-x86-multi-thread/flac.exe differ diff --git a/Bin/MSWin32-x86-multi-thread/sox.exe b/Bin/MSWin32-x86-multi-thread/sox.exe index 1ee979f58ec..689eb35c30e 100755 Binary files a/Bin/MSWin32-x86-multi-thread/sox.exe and b/Bin/MSWin32-x86-multi-thread/sox.exe differ diff --git a/Bin/aarch64-linux/alac b/Bin/aarch64-linux/alac new file mode 100755 index 00000000000..ae777774979 Binary files /dev/null and b/Bin/aarch64-linux/alac differ diff --git a/Bin/aarch64-linux/faad b/Bin/aarch64-linux/faad new file mode 100755 index 00000000000..04038d00fac Binary files /dev/null and b/Bin/aarch64-linux/faad differ diff --git a/Bin/aarch64-linux/flac b/Bin/aarch64-linux/flac new file mode 100755 index 00000000000..11f77919aa3 Binary files /dev/null and b/Bin/aarch64-linux/flac differ diff --git a/Bin/aarch64-linux/sox b/Bin/aarch64-linux/sox new file mode 100755 index 00000000000..9aad1a36353 Binary files /dev/null and b/Bin/aarch64-linux/sox differ diff --git a/Bin/aarch64-linux/wvunpack b/Bin/aarch64-linux/wvunpack new file mode 100755 index 00000000000..7e5657811ae Binary files /dev/null and b/Bin/aarch64-linux/wvunpack differ diff --git a/Bin/arm-linux/faad b/Bin/arm-linux/faad index bc2bd65a934..b56906f81c9 100755 Binary files a/Bin/arm-linux/faad and b/Bin/arm-linux/faad differ diff --git a/Bin/arm-linux/flac b/Bin/arm-linux/flac index 0bffea33421..5104e8a890f 100755 Binary files a/Bin/arm-linux/flac and b/Bin/arm-linux/flac differ diff --git a/Bin/arm-linux/sox b/Bin/arm-linux/sox index e40c6d32bc3..4dcd96840be 100755 Binary files a/Bin/arm-linux/sox and b/Bin/arm-linux/sox differ diff --git a/Bin/armhf-linux/faad b/Bin/armhf-linux/faad index 0acdefbf237..322fa15ac6c 100755 Binary files a/Bin/armhf-linux/faad and b/Bin/armhf-linux/faad differ diff --git a/Bin/armhf-linux/flac b/Bin/armhf-linux/flac index a3d842d92da..ccb4073a031 100755 Binary files a/Bin/armhf-linux/flac and b/Bin/armhf-linux/flac differ diff --git a/Bin/armhf-linux/mac b/Bin/armhf-linux/mac new file mode 100755 index 00000000000..02abbc2ea35 Binary files /dev/null and b/Bin/armhf-linux/mac differ diff --git a/Bin/armhf-linux/sox b/Bin/armhf-linux/sox index 7cc55e42b57..47ce0fd5c5f 100755 Binary files a/Bin/armhf-linux/sox and b/Bin/armhf-linux/sox differ diff --git a/Bin/darwin-x86_64/faad b/Bin/darwin-x86_64/faad new file mode 100755 index 00000000000..f639c2bcdb7 Binary files /dev/null and b/Bin/darwin-x86_64/faad differ diff --git a/Bin/darwin-x86_64/flac b/Bin/darwin-x86_64/flac new file mode 100755 index 00000000000..4477c9231ad Binary files /dev/null and b/Bin/darwin-x86_64/flac differ diff --git a/Bin/darwin-x86_64/sox b/Bin/darwin-x86_64/sox new file mode 100755 index 00000000000..273e043d4fe Binary files /dev/null and b/Bin/darwin-x86_64/sox differ diff --git a/Bin/darwin-x86_64/wvunpack b/Bin/darwin-x86_64/wvunpack new file mode 100755 index 00000000000..bb082d996ed Binary files /dev/null and b/Bin/darwin-x86_64/wvunpack differ diff --git a/Bin/darwin/check-update.pl b/Bin/darwin/check-update.pl index 6b25df0432b..a81275aa1d7 100755 --- a/Bin/darwin/check-update.pl +++ b/Bin/darwin/check-update.pl @@ -1,4 +1,4 @@ -#!/usr/bin/perl +#!/usr/bin/env perl # # This script checks whether we have an update ready to be installed @@ -14,6 +14,7 @@ BEGIN } use constant RESIZER => 0; +use constant SCANNER => 0; use Slim::Utils::Light; use Slim::Utils::OSDetect; diff --git a/Bin/darwin/faad b/Bin/darwin/faad index aa826ffbc12..c8fc8243134 100755 Binary files a/Bin/darwin/faad and b/Bin/darwin/faad differ diff --git a/Bin/dbish b/Bin/dbish index 779d4b981bf..6c51f6fa251 100755 --- a/Bin/dbish +++ b/Bin/dbish @@ -1,6 +1,5 @@ #!/usr/bin/perl -w -# $Id$ # Small wrapper to get at the database. use strict; diff --git a/Bin/i386-linux/faad b/Bin/i386-linux/faad index 1b393db9097..bbe1be5105a 100755 Binary files a/Bin/i386-linux/faad and b/Bin/i386-linux/faad differ diff --git a/Bin/i386-linux/flac b/Bin/i386-linux/flac index d53ec17dd60..4e6393f0bd9 100755 Binary files a/Bin/i386-linux/flac and b/Bin/i386-linux/flac differ diff --git a/Bin/i386-linux/sox b/Bin/i386-linux/sox index 62e1243d045..d2d00bd27fd 100755 Binary files a/Bin/i386-linux/sox and b/Bin/i386-linux/sox differ diff --git a/Bin/i86pc-solaris-thread-multi-64int/alac b/Bin/i86pc-solaris-thread-multi-64int/alac new file mode 100755 index 00000000000..dfd0926c90c Binary files /dev/null and b/Bin/i86pc-solaris-thread-multi-64int/alac differ diff --git a/Bin/i86pc-solaris-thread-multi-64int/faad b/Bin/i86pc-solaris-thread-multi-64int/faad new file mode 100755 index 00000000000..2a719c1501f Binary files /dev/null and b/Bin/i86pc-solaris-thread-multi-64int/faad differ diff --git a/Bin/i86pc-solaris-thread-multi-64int/flac b/Bin/i86pc-solaris-thread-multi-64int/flac new file mode 100755 index 00000000000..0bdc6ea40e3 Binary files /dev/null and b/Bin/i86pc-solaris-thread-multi-64int/flac differ diff --git a/Bin/i86pc-solaris-thread-multi-64int/lame b/Bin/i86pc-solaris-thread-multi-64int/lame new file mode 100755 index 00000000000..d994a5253a2 Binary files /dev/null and b/Bin/i86pc-solaris-thread-multi-64int/lame differ diff --git a/Bin/i86pc-solaris-thread-multi-64int/sox b/Bin/i86pc-solaris-thread-multi-64int/sox new file mode 100755 index 00000000000..bfff1b19079 Binary files /dev/null and b/Bin/i86pc-solaris-thread-multi-64int/sox differ diff --git a/Bin/i86pc-solaris-thread-multi-64int/wvunpack b/Bin/i86pc-solaris-thread-multi-64int/wvunpack new file mode 100755 index 00000000000..78b39acd1b3 Binary files /dev/null and b/Bin/i86pc-solaris-thread-multi-64int/wvunpack differ diff --git a/Bin/x86_64-linux/faad b/Bin/x86_64-linux/faad index 4b4cfe265f9..4ebe6dd1001 100755 Binary files a/Bin/x86_64-linux/faad and b/Bin/x86_64-linux/faad differ diff --git a/Bin/x86_64-linux/flac b/Bin/x86_64-linux/flac index d981e2d4a1c..cb4e0fb22b6 100755 Binary files a/Bin/x86_64-linux/flac and b/Bin/x86_64-linux/flac differ diff --git a/Bin/x86_64-linux/mac b/Bin/x86_64-linux/mac new file mode 100755 index 00000000000..40b63e7bffc Binary files /dev/null and b/Bin/x86_64-linux/mac differ diff --git a/Bin/x86_64-linux/sox b/Bin/x86_64-linux/sox index 6776aae36e2..c97199645d0 100755 Binary files a/Bin/x86_64-linux/sox and b/Bin/x86_64-linux/sox differ diff --git a/CPAN/IO/Socket/Socks.pm b/CPAN/IO/Socket/Socks.pm new file mode 100644 index 00000000000..7f6d0ddc043 --- /dev/null +++ b/CPAN/IO/Socket/Socks.pm @@ -0,0 +1,2606 @@ +package IO::Socket::Socks; + +use strict; +use IO::Select; +use Socket; +use Errno qw(EWOULDBLOCK EAGAIN EINPROGRESS ETIMEDOUT ECONNABORTED); +use Carp; +use vars qw( $SOCKET_CLASS @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION $SOCKS_ERROR $SOCKS5_RESOLVE $SOCKS4_RESOLVE $SOCKS_DEBUG %CODES ); +require Exporter; + +$VERSION = '0.74'; + +use constant { + SOCKS_WANT_READ => 20, + SOCKS_WANT_WRITE => 21, + ESOCKSPROTO => exists &Errno::EPROTO ? &Errno::EPROTO : 7000, +}; + +@ISA = ('Exporter', $SOCKET_CLASS||''); + +tie $SOCKET_CLASS, 'IO::Socket::Socks::SocketClassVar', $SOCKET_CLASS; +unless ($SOCKET_CLASS) { + if (eval { require IO::Socket::IP; IO::Socket::IP->VERSION(0.36) }) { + $SOCKET_CLASS = 'IO::Socket::IP'; + } + else { + $SOCKET_CLASS = 'IO::Socket::INET'; + } +} + +@EXPORT = qw( $SOCKS_ERROR SOCKS_WANT_READ SOCKS_WANT_WRITE ESOCKSPROTO ); +@EXPORT_OK = qw( + SOCKS5_VER + SOCKS4_VER + ADDR_IPV4 + ADDR_DOMAINNAME + ADDR_IPV6 + CMD_CONNECT + CMD_BIND + CMD_UDPASSOC + AUTHMECH_ANON + AUTHMECH_USERPASS + AUTHMECH_INVALID + AUTHREPLY_SUCCESS + AUTHREPLY_FAILURE + ISS_UNKNOWN_ADDRESS + ISS_BAD_VERSION + ISS_CANT_RESOLVE + REPLY_SUCCESS + REPLY_GENERAL_FAILURE + REPLY_CONN_NOT_ALLOWED + REPLY_NETWORK_UNREACHABLE + REPLY_HOST_UNREACHABLE + REPLY_CONN_REFUSED + REPLY_TTL_EXPIRED + REPLY_CMD_NOT_SUPPORTED + REPLY_ADDR_NOT_SUPPORTED + REQUEST_GRANTED + REQUEST_FAILED + REQUEST_REJECTED_IDENTD + REQUEST_REJECTED_USERID +); +%EXPORT_TAGS = (constants => [ 'SOCKS_WANT_READ', 'SOCKS_WANT_WRITE', @EXPORT_OK ]); +tie $SOCKS_ERROR, 'IO::Socket::Socks::ReadOnlyVar', IO::Socket::Socks::Error->new(); + +$SOCKS5_RESOLVE = 1; +$SOCKS4_RESOLVE = 0; +$SOCKS_DEBUG = $ENV{SOCKS_DEBUG}; + +use constant { + SOCKS5_VER => 5, + SOCKS4_VER => 4, + + ADDR_IPV4 => 1, + ADDR_DOMAINNAME => 3, + ADDR_IPV6 => 4, + + CMD_CONNECT => 1, + CMD_BIND => 2, + CMD_UDPASSOC => 3, + + AUTHMECH_ANON => 0, + + #AUTHMECH_GSSAPI => 1, + AUTHMECH_USERPASS => 2, + AUTHMECH_INVALID => 255, + + AUTHREPLY_SUCCESS => 0, + AUTHREPLY_FAILURE => 10, # to not intersect with other socks5 constants + + ISS_UNKNOWN_ADDRESS => 500, + ISS_BAD_VERSION => 501, + ISS_CANT_RESOLVE => 502, +}; + +$CODES{AUTHMECH}->[AUTHMECH_INVALID] = "No valid auth mechanisms"; +$CODES{AUTHREPLY}->[AUTHREPLY_FAILURE] = "Failed to authenticate"; + +# socks5 +use constant { + REPLY_SUCCESS => 0, + REPLY_GENERAL_FAILURE => 1, + REPLY_CONN_NOT_ALLOWED => 2, + REPLY_NETWORK_UNREACHABLE => 3, + REPLY_HOST_UNREACHABLE => 4, + REPLY_CONN_REFUSED => 5, + REPLY_TTL_EXPIRED => 6, + REPLY_CMD_NOT_SUPPORTED => 7, + REPLY_ADDR_NOT_SUPPORTED => 8, +}; + +$CODES{REPLY}->{&REPLY_SUCCESS} = "Success"; +$CODES{REPLY}->{&REPLY_GENERAL_FAILURE} = "General failure"; +$CODES{REPLY}->{&REPLY_CONN_NOT_ALLOWED} = "Not allowed"; +$CODES{REPLY}->{&REPLY_NETWORK_UNREACHABLE} = "Network unreachable"; +$CODES{REPLY}->{&REPLY_HOST_UNREACHABLE} = "Host unreachable"; +$CODES{REPLY}->{&REPLY_CONN_REFUSED} = "Connection refused"; +$CODES{REPLY}->{&REPLY_TTL_EXPIRED} = "TTL expired"; +$CODES{REPLY}->{&REPLY_CMD_NOT_SUPPORTED} = "Command not supported"; +$CODES{REPLY}->{&REPLY_ADDR_NOT_SUPPORTED} = "Address not supported"; + +# socks4 +use constant { + REQUEST_GRANTED => 90, + REQUEST_FAILED => 91, + REQUEST_REJECTED_IDENTD => 92, + REQUEST_REJECTED_USERID => 93, +}; + +$CODES{REPLY}->{&REQUEST_GRANTED} = "request granted"; +$CODES{REPLY}->{&REQUEST_FAILED} = "request rejected or failed"; +$CODES{REPLY}->{&REQUEST_REJECTED_IDENTD} = "request rejected because SOCKS server cannot connect to identd on the client"; +$CODES{REPLY}->{&REQUEST_REJECTED_USERID} = "request rejected because the client program and identd report different user-ids"; + +# queue +use constant { + Q_SUB => 0, + Q_ARGS => 1, + Q_BUF => 2, + Q_READS => 3, + Q_SENDS => 4, + Q_OKCB => 5, + Q_DEBUGS => 6, +}; + +our $CAN_CHANGE_SOCKET = 1; +sub new_from_fd { + my ($class, $sock, %arg) = @_; + + bless $sock, $class; + + $sock->autoflush(1); + if (exists $arg{Timeout}) { + ${*$sock}{'io_socket_timeout'} = delete $arg{Timeout}; + } + + scalar(%arg) or return $sock; + + # do not allow to create new socket + local $CAN_CHANGE_SOCKET = 0; + $sock->configure(\%arg) || $SOCKS_ERROR == SOCKS_WANT_WRITE || return; + $sock; +} + +*new_from_socket = \&new_from_fd; + +sub start_SOCKS { + my ($class, $sock, %arg) = @_; + + bless $sock, $class; + + $sock->autoflush(1); + if (exists $arg{Timeout}) { + ${*$sock}{'io_socket_timeout'} = delete $arg{Timeout}; + } + + ${*$sock}->{SOCKS} = { RequireAuth => 0 }; + + $SOCKS_ERROR->set(); + return $sock->command(%arg) ? $sock : undef; +} + +sub socket { + my $self = shift; + + return $self unless $CAN_CHANGE_SOCKET; + return $self->SUPER::socket(@_); +} + +sub configure { + my $self = shift; + my $args = shift; + + $self->_configure($args) + or return; + + ${*$self}->{SOCKS}->{ProxyAddr} = ( + exists($args->{ProxyAddr}) + ? delete($args->{ProxyAddr}) + : undef + ); + + ${*$self}->{SOCKS}->{ProxyPort} = ( + exists($args->{ProxyPort}) + ? delete($args->{ProxyPort}) + : undef + ); + + ${*$self}->{SOCKS}->{COMMAND} = []; + + if (exists($args->{Listen})) { + $args->{LocalAddr} = ${*$self}->{SOCKS}->{ProxyAddr}; + $args->{LocalPort} = ${*$self}->{SOCKS}->{ProxyPort}; + $args->{Reuse} = 1; + ${*$self}->{SOCKS}->{Listen} = 1; + } + elsif (${*$self}->{SOCKS}->{ProxyAddr} && ${*$self}->{SOCKS}->{ProxyPort}) { + $args->{PeerAddr} = ${*$self}->{SOCKS}->{ProxyAddr}; + $args->{PeerPort} = ${*$self}->{SOCKS}->{ProxyPort}; + } + + unless (defined ${*$self}->{SOCKS}->{TCP}) { + $args->{Proto} = "tcp"; + $args->{Type} = SOCK_STREAM; + } + elsif (!defined $args->{Proto}) { + $args->{Proto} = "udp"; + $args->{Type} = SOCK_DGRAM; + } + + $SOCKS_ERROR->set(); + unless ($self->SUPER::configure($args)) { + if ($SOCKS_ERROR == undef) { + $SOCKS_ERROR->set($!, $@); + } + return; + } + + return $self; +} + +sub _configure { + my $self = shift; + my $args = shift; + + ${*$self}->{SOCKS}->{Version} = ( + exists($args->{SocksVersion}) + ? ( + $args->{SocksVersion} == 4 + || $args->{SocksVersion} == 5 + || ( exists $args->{Listen} + && ref $args->{SocksVersion} eq 'ARRAY' + && _validate_multi_version($args->{SocksVersion})) + ? delete($args->{SocksVersion}) + : croak("Unsupported socks version specified. Should be 4 or 5") + ) + : 5 + ); + + ${*$self}->{SOCKS}->{AuthType} = ( + exists($args->{AuthType}) + ? delete($args->{AuthType}) + : "none" + ); + + ${*$self}->{SOCKS}->{RequireAuth} = ( + exists($args->{RequireAuth}) + ? delete($args->{RequireAuth}) + : 0 + ); + + ${*$self}->{SOCKS}->{UserAuth} = ( + exists($args->{UserAuth}) + ? delete($args->{UserAuth}) + : undef + ); + + ${*$self}->{SOCKS}->{Username} = ( + exists($args->{Username}) ? delete($args->{Username}) + : ( + (${*$self}->{SOCKS}->{AuthType} eq "none") ? undef + : croak("If you set AuthType to userpass, then you must provide a username.") + ) + ); + + ${*$self}->{SOCKS}->{Password} = ( + exists($args->{Password}) ? delete($args->{Password}) + : ( + (${*$self}->{SOCKS}->{AuthType} eq "none") ? undef + : croak("If you set AuthType to userpass, then you must provide a password.") + ) + ); + + ${*$self}->{SOCKS}->{Debug} = ( + exists($args->{SocksDebug}) + ? delete($args->{SocksDebug}) + : $SOCKS_DEBUG + ); + + ${*$self}->{SOCKS}->{Resolve} = ( + exists($args->{SocksResolve}) + ? delete($args->{SocksResolve}) + : undef + ); + + ${*$self}->{SOCKS}->{AuthMethods} = [ 0, 0, 0 ]; + ${*$self}->{SOCKS}->{AuthMethods}->[AUTHMECH_ANON] = 1 + unless ${*$self}->{SOCKS}->{RequireAuth}; + + #${*$self}->{SOCKS}->{AuthMethods}->[AUTHMECH_GSSAPI] = 1 + # if (${*$self}->{SOCKS}->{AuthType} eq "gssapi"); + ${*$self}->{SOCKS}->{AuthMethods}->[AUTHMECH_USERPASS] = 1 + if ( + (!exists($args->{Listen}) && (${*$self}->{SOCKS}->{AuthType} eq "userpass")) + || (exists($args->{Listen}) + && defined(${*$self}->{SOCKS}->{UserAuth})) + ); + + if (exists($args->{BindAddr}) && exists($args->{BindPort})) { + ${*$self}->{SOCKS}->{CmdAddr} = delete($args->{BindAddr}); + ${*$self}->{SOCKS}->{CmdPort} = delete($args->{BindPort}); + ${*$self}->{SOCKS}->{Bind} = 1; + } + elsif (exists($args->{UdpAddr}) && exists($args->{UdpPort})) { + if (${*$self}->{SOCKS}->{Version} == 4) { + croak("Socks v4 doesn't support UDP association"); + } + ${*$self}->{SOCKS}->{CmdAddr} = delete($args->{UdpAddr}); + ${*$self}->{SOCKS}->{CmdPort} = delete($args->{UdpPort}); + ${*$self}->{SOCKS}->{TCP} = __PACKAGE__->new( # TCP backend for UDP socket + Timeout => $args->{Timeout}, + Proto => 'tcp', + PeerAddr => $args->{ProxyAddr}, + PeerPort => $args->{ProxyPort}, + exists $args->{Blocking} ? + (Blocking => $args->{Blocking}) : () + ) or return; + } + elsif (exists($args->{ConnectAddr}) && exists($args->{ConnectPort})) { + ${*$self}->{SOCKS}->{CmdAddr} = delete($args->{ConnectAddr}); + ${*$self}->{SOCKS}->{CmdPort} = delete($args->{ConnectPort}); + } + + return 1; +} + +sub version { + my $self = shift; + return ${*$self}->{SOCKS}->{Version}; +} + +sub connect { + my $self = shift; + + croak("Undefined IO::Socket::Socks object passed to connect.") + unless defined($self); + + my $ok = + defined(${*$self}->{SOCKS}->{TCP}) + ? 1 + : $self->SUPER::connect(@_); + + if (($! == EINPROGRESS || $! == EWOULDBLOCK) && + (${*$self}->{SOCKS}->{TCP} || $self)->blocking == 0) { + ${*$self}->{SOCKS}->{_in_progress} = 1; + $SOCKS_ERROR->set(SOCKS_WANT_WRITE, 'Socks want write'); + } + elsif (!$ok) { + $SOCKS_ERROR->set($!, $@ = "Connection to proxy failed: $!"); + return; + } + else { + # connect() may be called several times by SUPER class + $SOCKS_ERROR->set(); + } + + return $ok # proxy address was not specified, so do not make socks handshake + unless ${*$self}->{SOCKS}->{ProxyAddr} && ${*$self}->{SOCKS}->{ProxyPort}; + + $self->_connect(); +} + +sub _connect { + my $self = shift; + ${*$self}->{SOCKS}->{ready} = 0; + + if (${*$self}->{SOCKS}->{Version} == 4) { + ${*$self}->{SOCKS}->{queue} = [ + + # [sub, [@args], buf, [@reads], sends_cnt] + [ '_socks4_connect_command', [ ${*$self}->{SOCKS}->{Bind} ? CMD_BIND : CMD_CONNECT ], undef, [], 0 ], + [ '_socks4_connect_reply', [], undef, [], 0 ] + ]; + } + else { + ${*$self}->{SOCKS}->{queue} = [ + [ '_socks5_connect', [], undef, [], 0 ], + [ '_socks5_connect_if_auth', [], undef, [], 0 ], + [ + '_socks5_connect_command', + [ + ${*$self}->{SOCKS}->{Bind} ? CMD_BIND + : ${*$self}->{SOCKS}->{TCP} ? CMD_UDPASSOC + : CMD_CONNECT + ], + undef, + [], + 0 + ], + [ '_socks5_connect_reply', [], undef, [], 0 ] + ]; + } + + if (delete ${*$self}->{SOCKS}->{_in_progress}) { # socket connection not estabilished yet + if ($self->isa('IO::Socket::IP')) { + # IO::Socket::IP requires multiple connect calls + # when performing non-blocking multi-homed connect + unshift @{ ${*$self}->{SOCKS}->{queue} }, ['_socket_connect', [], undef, [], 0]; + + # IO::Socket::IP::connect() returns false for non-blocking connections in progress + # IO::Socket::INET::connect() returns true for non-blocking connections in progress + # LOL + return; # connect() return value + } + } + else { + defined($self->_run_queue()) + or return; + } + + return $self; +} + +sub _socket_connect { + my $self = shift; + my $sock = ${*$self}->{SOCKS}->{TCP} || $self; + + return 1 if $sock->SUPER::connect(); + if ($! == EINPROGRESS || $! == EWOULDBLOCK) { + $SOCKS_ERROR->set(SOCKS_WANT_WRITE, 'Socks want write'); + return -1; + } + + $SOCKS_ERROR->set($!, $@ = "Connection to proxy failed: $!"); + return; +} + +sub _run_queue { + # run tasks from queue, return undef on error, -1 if one of the task + # returned not completed because of the possible blocking on network operation + my $self = shift; + + my $retval; + my $sub; + + while (my $elt = ${*$self}->{SOCKS}->{queue}[0]) { + $sub = $elt->[Q_SUB]; + $retval = $self->$sub(@{ $elt->[Q_ARGS] }); + unless (defined $retval) { + ${*$self}->{SOCKS}->{queue} = []; + ${*$self}->{SOCKS}->{queue_results} = {}; + last; + } + + last if ($retval == -1); + ${*$self}->{SOCKS}->{queue_results}{$sub} = $retval; + if ($elt->[Q_OKCB]) { + $elt->[Q_OKCB]->(); + } + shift @{ ${*$self}->{SOCKS}->{queue} }; + } + + if (defined($retval) && !@{ ${*$self}->{SOCKS}->{queue} }) { + ${*$self}->{SOCKS}->{queue_results} = {}; + ${*$self}->{SOCKS}->{ready} = $SOCKS_ERROR ? 0 : 1; + } + + return $retval; +} + +sub ready { + my $self = shift; + + $self->_run_queue(); + return ${*$self}->{SOCKS}->{ready}; +} + +sub _socks5_connect { + my $self = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + my $sock = + defined(${*$self}->{SOCKS}->{TCP}) + ? ${*$self}->{SOCKS}->{TCP} + : $self; + + #-------------------------------------------------------------------------- + # Send the auth mechanisms + #-------------------------------------------------------------------------- + # +----+----------+----------+ + # |VER | NMETHODS | METHODS | + # +----+----------+----------+ + # | 1 | 1 | 1 to 255 | + # +----+----------+----------+ + + my $nmethods = 0; + my $methods; + foreach my $method (0 .. $#{ ${*$self}->{SOCKS}->{AuthMethods} }) { + if (${*$self}->{SOCKS}->{AuthMethods}->[$method] == 1) { + $methods .= pack('C', $method); + $nmethods++; + } + } + + my $reply; + $reply = $sock->_socks_send(pack('CCa*', SOCKS5_VER, $nmethods, $methods), ++$sends) + or return _fail($reply); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => SOCKS5_VER, + nmethods => $nmethods, + methods => join('', unpack("C$nmethods", $methods)) + ); + $debug->show('Client Send: '); + } + + #-------------------------------------------------------------------------- + # Read the reply + #-------------------------------------------------------------------------- + # +----+--------+ + # |VER | METHOD | + # +----+--------+ + # | 1 | 1 | + # +----+--------+ + + $reply = $sock->_socks_read(2, ++$reads) + or return _fail($reply); + + my ($version, $auth_method) = unpack('CC', $reply); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => $version, + method => $auth_method + ); + $debug->show('Client Recv: '); + } + + if ($auth_method == AUTHMECH_INVALID) { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(AUTHMECH_INVALID, $@ = $CODES{AUTHMECH}->[$auth_method]); + return; + } + + return $auth_method; +} + +sub _socks5_connect_if_auth { + my $self = shift; + if (${*$self}->{SOCKS}->{queue_results}{'_socks5_connect'} != AUTHMECH_ANON) { + unshift @{ ${*$self}->{SOCKS}->{queue} }, [ '_socks5_connect_auth', [], undef, [], 0 ]; + (${*$self}->{SOCKS}->{queue}[0], ${*$self}->{SOCKS}->{queue}[1]) = (${*$self}->{SOCKS}->{queue}[1], ${*$self}->{SOCKS}->{queue}[0]); + } + + 1; +} + +sub _socks5_connect_auth { + # rfc1929 + my $self = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + my $sock = + defined(${*$self}->{SOCKS}->{TCP}) + ? ${*$self}->{SOCKS}->{TCP} + : $self; + + #-------------------------------------------------------------------------- + # Send the auth + #-------------------------------------------------------------------------- + # +----+------+----------+------+----------+ + # |VER | ULEN | UNAME | PLEN | PASSWD | + # +----+------+----------+------+----------+ + # | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + # +----+------+----------+------+----------+ + + my $uname = ${*$self}->{SOCKS}->{Username}; + my $passwd = ${*$self}->{SOCKS}->{Password}; + my $ulen = length($uname); + my $plen = length($passwd); + my $reply; + $reply = $sock->_socks_send(pack("CCa${ulen}Ca*", 1, $ulen, $uname, $plen, $passwd), ++$sends) + or return _fail($reply); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => 1, + ulen => $ulen, + uname => $uname, + plen => $plen, + passwd => $passwd + ); + $debug->show('Client Send: '); + } + + #-------------------------------------------------------------------------- + # Read the reply + #-------------------------------------------------------------------------- + # +----+--------+ + # |VER | STATUS | + # +----+--------+ + # | 1 | 1 | + # +----+--------+ + + $reply = $sock->_socks_read(2, ++$reads) + or return _fail($reply); + + my ($ver, $status) = unpack('CC', $reply); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => $ver, + status => $status + ); + $debug->show('Client Recv: '); + } + + if ($status != AUTHREPLY_SUCCESS) { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(AUTHREPLY_FAILURE, $@ = "Authentication failed with SOCKS5 proxy"); + return; + } + + return 1; +} + +sub _socks5_connect_command { + my $self = shift; + my $command = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + my $resolve = defined(${*$self}->{SOCKS}->{Resolve}) ? ${*$self}->{SOCKS}->{Resolve} : $SOCKS5_RESOLVE; + my $sock = + defined(${*$self}->{SOCKS}->{TCP}) + ? ${*$self}->{SOCKS}->{TCP} + : $self; + + #-------------------------------------------------------------------------- + # Send the command + #-------------------------------------------------------------------------- + # +----+-----+-------+------+----------+----------+ + # |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + # +----+-----+-------+------+----------+----------+ + # | 1 | 1 | X'00' | 1 | Variable | 2 | + # +----+-----+-------+------+----------+----------+ + + my ($atyp, $dstaddr) = $resolve ? (ADDR_DOMAINNAME, ${*$self}->{SOCKS}->{CmdAddr}) : _resolve(${*$self}->{SOCKS}->{CmdAddr}) + or $SOCKS_ERROR->set(ISS_CANT_RESOLVE, $@ = "Can't resolve `" . ${*$self}->{SOCKS}->{CmdAddr} . "'"), return; + my $hlen = length($dstaddr) if $resolve; + my $dstport = pack('n', ${*$self}->{SOCKS}->{CmdPort}); + my $reply; + $reply = $sock->_socks_send(pack('C4', SOCKS5_VER, $command, 0, $atyp) . (defined($hlen) ? pack('C', $hlen) : '') . $dstaddr . $dstport, ++$sends) + or return _fail($reply); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => SOCKS5_VER, + cmd => $command, + rsv => 0, + atyp => $atyp + ); + $debug->add(hlen => $hlen) if defined $hlen; + $debug->add( + dstaddr => $resolve ? $dstaddr : _addr_ntoa($dstaddr, $atyp), + dstport => ${*$self}->{SOCKS}->{CmdPort} + ); + $debug->show('Client Send: '); + } + + return 1; +} + +sub _socks5_connect_reply { + my $self = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + my $sock = + defined(${*$self}->{SOCKS}->{TCP}) + ? ${*$self}->{SOCKS}->{TCP} + : $self; + + #-------------------------------------------------------------------------- + # Read the reply + #-------------------------------------------------------------------------- + # +----+-----+-------+------+----------+----------+ + # |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + # +----+-----+-------+------+----------+----------+ + # | 1 | 1 | X'00' | 1 | Variable | 2 | + # +----+-----+-------+------+----------+----------+ + + my $reply; + $reply = $sock->_socks_read(4, ++$reads) + or return _fail($reply); + + my ($ver, $rep, $rsv, $atyp) = unpack('C4', $reply); + + if ($debug) { + $debug->add( + ver => $ver, + rep => $rep, + rsv => $rsv, + atyp => $atyp + ); + } + + my ($bndaddr, $bndport); + + if ($atyp == ADDR_DOMAINNAME) { + length($reply = $sock->_socks_read(1, ++$reads)) + or return _fail($reply); + + my $hlen = unpack('C', $reply); + $bndaddr = $sock->_socks_read($hlen, ++$reads) + or return _fail($bndaddr); + + if ($debug) { + $debug->add(hlen => $hlen); + } + } + elsif ($atyp == ADDR_IPV4) { + $bndaddr = $sock->_socks_read(4, ++$reads) + or return _fail($bndaddr); + } + elsif ($atyp == ADDR_IPV6) { + $bndaddr = $sock->_socks_read(16, ++$reads) + or return _fail($bndaddr); + } + else { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(ISS_UNKNOWN_ADDRESS, $@ = "Unsupported address type returned by socks server: $atyp"); + return; + } + + $reply = $sock->_socks_read(2, ++$reads) + or return _fail($reply); + $bndport = unpack('n', $reply); + + ${*$self}->{SOCKS}->{DstAddrType} = $atyp; + ${*$self}->{SOCKS}->{DstAddr} = $bndaddr; + ${*$self}->{SOCKS}->{DstPort} = $bndport; + + if ($debug && !$self->_debugged(++$debugs)) { + my ($addr) = $self->dst; + $debug->add( + bndaddr => $addr, + bndport => $bndport + ); + $debug->show('Client Recv: '); + } + + if ($rep != REPLY_SUCCESS) { + $! = ESOCKSPROTO; + unless (exists $CODES{REPLY}->{$rep}) { + $rep = REPLY_GENERAL_FAILURE; + } + $SOCKS_ERROR->set($rep, $@ = $CODES{REPLY}->{$rep}); + return; + } + + return 1; +} + + +sub _socks4_connect_command { + # http://ss5.sourceforge.net/socks4.protocol.txt + # http://ss5.sourceforge.net/socks4A.protocol.txt + my $self = shift; + my $command = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + my $resolve = defined(${*$self}->{SOCKS}->{Resolve}) ? ${*$self}->{SOCKS}->{Resolve} : $SOCKS4_RESOLVE; + + #-------------------------------------------------------------------------- + # Send the command + #-------------------------------------------------------------------------- + # +-----+-----+----------+---------------+----------+------+ + # | VER | CMD | DST.PORT | DST.ADDR | USERID | NULL | + # +-----+-----+----------+---------------+----------+------+ + # | 1 | 1 | 2 | 4 | variable | 1 | + # +-----+-----+----------+---------------+----------+------+ + + my $dstaddr = $resolve ? inet_aton('0.0.0.1') : inet_aton(${*$self}->{SOCKS}->{CmdAddr}) + or $SOCKS_ERROR->set(ISS_CANT_RESOLVE, $@ = "Can't resolve `" . ${*$self}->{SOCKS}->{CmdAddr} . "'"), return; + my $dstport = pack('n', ${*$self}->{SOCKS}->{CmdPort}); + my $userid = ${*$self}->{SOCKS}->{Username} || ''; + my $dsthost = ''; + if ($resolve) { # socks4a + $dsthost = ${*$self}->{SOCKS}->{CmdAddr} . pack('C', 0); + } + + my $reply; + $reply = $self->_socks_send(pack('CC', SOCKS4_VER, $command) . $dstport . $dstaddr . $userid . pack('C', 0) . $dsthost, ++$sends) + or return _fail($reply); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => SOCKS4_VER, + cmd => $command, + dstport => ${*$self}->{SOCKS}->{CmdPort}, + dstaddr => length($dstaddr) == 4 ? inet_ntoa($dstaddr) : undef, + userid => $userid, + null => 0 + ); + if ($dsthost) { + $debug->add( + dsthost => ${*$self}->{SOCKS}->{CmdAddr}, + null => 0 + ); + } + $debug->show('Client Send: '); + } + + return 1; +} + +sub _socks4_connect_reply { + my $self = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + + #-------------------------------------------------------------------------- + # Read the reply + #-------------------------------------------------------------------------- + # +-----+-----+----------+---------------+ + # | VER | REP | BND.PORT | BND.ADDR | + # +-----+-----+----------+---------------+ + # | 1 | 1 | 2 | 4 | + # +-----+-----+----------+---------------+ + + my $reply; + $reply = $self->_socks_read(8, ++$reads) + or return _fail($reply); + + my ($ver, $rep, $bndport) = unpack('CCn', $reply); + substr($reply, 0, 4) = ''; + + ${*$self}->{SOCKS}->{DstAddrType} = ADDR_IPV4; + ${*$self}->{SOCKS}->{DstAddr} = $reply; + ${*$self}->{SOCKS}->{DstPort} = $bndport; + + if ($debug && !$self->_debugged(++$debugs)) { + my ($addr) = $self->dst; + + $debug->add( + ver => $ver, + rep => $rep, + bndport => $bndport, + bndaddr => $addr + ); + $debug->show('Client Recv: '); + } + + if ($rep != REQUEST_GRANTED) { + $! = ESOCKSPROTO; + unless (exists $CODES{REPLY}->{$rep}) { + $rep = REQUEST_FAILED; + } + $SOCKS_ERROR->set($rep, $@ = $CODES{REPLY}->{$rep}); + return; + } + + return 1; +} + +sub accept { + my $self = shift; + + croak("Undefined IO::Socket::Socks object passed to accept.") + unless defined($self); + + if (${*$self}->{SOCKS}->{Listen}) { + my $client = $self->SUPER::accept(@_); + + if (!$client) { + if ($! == EAGAIN || $! == EWOULDBLOCK) { + $SOCKS_ERROR->set(SOCKS_WANT_READ, "Socks want read"); + } + else { + $SOCKS_ERROR->set($!, $@ = "Proxy accept new client failed: $!"); + } + return; + } + + my $ver = + ref ${*$self}->{SOCKS}->{Version} + ? @{ ${*$self}->{SOCKS}->{Version} } > 1 + ? ${*$self}->{SOCKS}->{Version} + : ${*$self}->{SOCKS}->{Version}->[0] + : ${*$self}->{SOCKS}->{Version}; + + # inherit some socket parameters + ${*$client}->{SOCKS}->{Debug} = ${*$self}->{SOCKS}->{Debug}; + ${*$client}->{SOCKS}->{Version} = $ver; + ${*$client}->{SOCKS}->{AuthMethods} = ${*$self}->{SOCKS}->{AuthMethods}; + ${*$client}->{SOCKS}->{UserAuth} = ${*$self}->{SOCKS}->{UserAuth}; + ${*$client}->{SOCKS}->{Resolve} = ${*$self}->{SOCKS}->{Resolve}; + ${*$client}->{SOCKS}->{ready} = 0; + $client->blocking($self->blocking); # temporarily + + if (ref $ver) { + ${*$client}->{SOCKS}->{queue} = [ [ '_socks_accept', [], undef, [], 0 ] ]; + } + elsif ($ver == 4) { + ${*$client}->{SOCKS}->{queue} = [ [ '_socks4_accept_command', [], undef, [], 0 ] ]; + + } + else { + ${*$client}->{SOCKS}->{queue} = [ + [ '_socks5_accept', [], undef, [], 0 ], + [ '_socks5_accept_if_auth', [], undef, [], 0 ], + [ '_socks5_accept_command', [], undef, [], 0 ] + ]; + } + + defined($client->_run_queue()) + or return; + + $client->blocking(1); # new socket should be in blocking mode + return $client; + } + else { + ${*$self}->{SOCKS}->{ready} = 0; + if ({*$self}->{SOCKS}->{Version} == 4) { + push @{ ${*$self}->{SOCKS}->{queue} }, [ '_socks4_connect_reply', [], undef, [], 0 ]; + } + else { + push @{ ${*$self}->{SOCKS}->{queue} }, [ '_socks5_connect_reply', [], undef, [], 0 ]; + } + + defined($self->_run_queue()) + or return; + + return $self; + } +} + +sub _socks_accept { + # when 4 and 5 version allowed + my $self = shift; + + my $request; + $request = $self->_socks_read(1, 0) + or return _fail($request); + + my $ver = unpack('C', $request); + if ($ver == 4) { + ${*$self}->{SOCKS}->{Version} = 4; + push @{ ${*$self}->{SOCKS}->{queue} }, [ '_socks4_accept_command', [$ver], undef, [], 0 ]; + } + elsif ($ver == 5) { + ${*$self}->{SOCKS}->{Version} = 5; + push @{ ${*$self}->{SOCKS}->{queue} }, + [ '_socks5_accept', [$ver], undef, [], 0 ], + [ '_socks5_accept_if_auth', [], undef, [], 0 ], + [ '_socks5_accept_command', [], undef, [], 0 ]; + } + else { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(ISS_BAD_VERSION, $@ = "Socks version should be 4 or 5, $ver recieved"); + return; + } + + 1; +} + +sub _socks5_accept { + my ($self, $ver) = @_; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + + #-------------------------------------------------------------------------- + # Read the auth mechanisms + #-------------------------------------------------------------------------- + # +----+----------+----------+ + # |VER | NMETHODS | METHODS | + # +----+----------+----------+ + # | 1 | 1 | 1 to 255 | + # +----+----------+----------+ + + my $request; + $request = $self->_socks_read($ver ? 1 : 2, ++$reads) + or return _fail($request); + + unless ($ver) { + $ver = unpack('C', $request); + } + my $nmethods = unpack('C', substr($request, -1, 1)); + + $request = $self->_socks_read($nmethods, ++$reads) + or return _fail($request); + + my @methods = unpack('C' x $nmethods, $request); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => $ver, + nmethods => $nmethods, + methods => join('', @methods) + ); + $debug->show('Server Recv: '); + } + + if ($ver != SOCKS5_VER) { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(ISS_BAD_VERSION, $@ = "Socks version should be 5, $ver recieved"); + return; + } + + if ($nmethods == 0) { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(AUTHMECH_INVALID, $@ = "No auth methods sent"); + return; + } + + my $authmech; + + foreach my $method (@methods) { + if (${*$self}->{SOCKS}->{AuthMethods}->[$method] == 1) { + $authmech = $method; + last; + } + } + + if (!defined($authmech)) { + $authmech = AUTHMECH_INVALID; + } + + #-------------------------------------------------------------------------- + # Send the reply + #-------------------------------------------------------------------------- + # +----+--------+ + # |VER | METHOD | + # +----+--------+ + # | 1 | 1 | + # +----+--------+ + + $request = $self->_socks_send(pack('CC', SOCKS5_VER, $authmech), ++$sends) + or return _fail($request); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => SOCKS5_VER, + method => $authmech + ); + $debug->show('Server Send: '); + } + + if ($authmech == AUTHMECH_INVALID) { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(AUTHMECH_INVALID, $@ = "No available auth methods"); + return; + } + + return $authmech; +} + +sub _socks5_accept_if_auth { + my $self = shift; + + if (${*$self}->{SOCKS}->{queue_results}{'_socks5_accept'} == AUTHMECH_USERPASS) { + unshift @{ ${*$self}->{SOCKS}->{queue} }, [ '_socks5_accept_auth', [], undef, [], 0 ]; + (${*$self}->{SOCKS}->{queue}[0], ${*$self}->{SOCKS}->{queue}[1]) = (${*$self}->{SOCKS}->{queue}[1], ${*$self}->{SOCKS}->{queue}[0]); + } + + 1; +} + +sub _socks5_accept_auth { + my $self = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + + #-------------------------------------------------------------------------- + # Read the auth + #-------------------------------------------------------------------------- + # +----+------+----------+------+----------+ + # |VER | ULEN | UNAME | PLEN | PASSWD | + # +----+------+----------+------+----------+ + # | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + # +----+------+----------+------+----------+ + + my $request; + $request = $self->_socks_read(2, ++$reads) + or return _fail($request); + + my ($ver, $ulen) = unpack('CC', $request); + $request = $self->_socks_read($ulen + 1, ++$reads) + or return _fail($request); + + my $uname = substr($request, 0, $ulen); + my $plen = unpack('C', substr($request, $ulen)); + my $passwd; + $passwd = $self->_socks_read($plen, ++$reads) + or return _fail($passwd); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => $ver, + ulen => $ulen, + uname => $uname, + plen => $plen, + passwd => $passwd + ); + $debug->show('Server Recv: '); + } + + my $status = 1; + if (defined(${*$self}->{SOCKS}->{UserAuth})) { + $status = &{ ${*$self}->{SOCKS}->{UserAuth} }($uname, $passwd); + } + + #-------------------------------------------------------------------------- + # Send the reply + #-------------------------------------------------------------------------- + # +----+--------+ + # |VER | STATUS | + # +----+--------+ + # | 1 | 1 | + # +----+--------+ + + $status = $status ? AUTHREPLY_SUCCESS : 1; #XXX AUTHREPLY_FAILURE broken + $request = $self->_socks_send(pack('CC', 1, $status), ++$sends) + or return _fail($request); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => 1, + status => $status + ); + $debug->show('Server Send: '); + } + + if ($status != AUTHREPLY_SUCCESS) { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(AUTHREPLY_FAILURE, $@ = "Authentication failed with SOCKS5 proxy"); + return; + } + + return 1; +} + +sub _socks5_accept_command { + my $self = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + + @{ ${*$self}->{SOCKS}->{COMMAND} } = (); + + #-------------------------------------------------------------------------- + # Read the command + #-------------------------------------------------------------------------- + # +----+-----+-------+------+----------+----------+ + # |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + # +----+-----+-------+------+----------+----------+ + # | 1 | 1 | X'00' | 1 | Variable | 2 | + # +----+-----+-------+------+----------+----------+ + + my $request; + $request = $self->_socks_read(4, ++$reads) + or return _fail($request); + + my ($ver, $cmd, $rsv, $atyp) = unpack('CCCC', $request); + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => $ver, + cmd => $cmd, + rsv => $rsv, + atyp => $atyp + ); + } + + my $dstaddr; + if ($atyp == ADDR_DOMAINNAME) { + length($request = $self->_socks_read(1, ++$reads)) + or return _fail($request); + + my $hlen = unpack('C', $request); + $dstaddr = $self->_socks_read($hlen, ++$reads) + or return _fail($dstaddr); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add(hlen => $hlen); + } + } + elsif ($atyp == ADDR_IPV4) { + $request = $self->_socks_read(4, ++$reads) + or return _fail($request); + + $dstaddr = length($request) == 4 ? inet_ntoa($request) : undef; + } + elsif ($atyp == ADDR_IPV6) { + $request = $self->_socks_read(16, ++$reads) + or return _fail($request); + + $dstaddr = length($request) == 16 ? Socket::inet_ntop(AF_INET6, $request) : undef; + } + else { # unknown address type - how many bytes to read? + push @{${*$self}->{SOCKS}->{queue}}, [ + '_socks5_accept_command_reply', [ REPLY_ADDR_NOT_SUPPORTED, '0.0.0.0', 0 ], undef, [], 0, + sub { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(REPLY_ADDR_NOT_SUPPORTED, $@ = $CODES{REPLY}->{REPLY_ADDR_NOT_SUPPORTED}); + } + ]; + + return 0; + } + + $request = $self->_socks_read(2, ++$reads) + or return _fail($request); + + my $dstport = unpack('n', $request); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + dstaddr => $dstaddr, + dstport => $dstport + ); + $debug->show('Server Recv: '); + } + + @{ ${*$self}->{SOCKS}->{COMMAND} } = ($cmd, $dstaddr, $dstport, $atyp); + + return 1; +} + +sub _socks5_accept_command_reply { + my $self = shift; + my $reply = shift; + my $host = shift; + my $port = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my $resolve = defined(${*$self}->{SOCKS}->{Resolve}) ? ${*$self}->{SOCKS}->{Resolve} : $SOCKS5_RESOLVE; + my ($reads, $sends, $debugs) = (0, 0, 0); + + if (!defined($reply) || !defined($host) || !defined($port)) { + croak("You must provide a reply, host, and port on the command reply."); + } + + #-------------------------------------------------------------------------- + # Send the reply + #-------------------------------------------------------------------------- + # +----+-----+-------+------+----------+----------+ + # |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + # +----+-----+-------+------+----------+----------+ + # | 1 | 1 | X'00' | 1 | Variable | 2 | + # +----+-----+-------+------+----------+----------+ + + my ($atyp, $bndaddr) = $resolve ? _resolve($host) : (ADDR_DOMAINNAME, $host) + or $SOCKS_ERROR->set(ISS_CANT_RESOLVE, $@ = "Can't resolve `$host'"), return; + my $hlen = $resolve ? undef : length($bndaddr); + my $rc; + $rc = $self->_socks_send(pack('CCCC', SOCKS5_VER, $reply, 0, $atyp) . ($resolve ? '' : pack('C', $hlen)) . $bndaddr . pack('n', $port), ++$sends) + or return _fail($rc); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => SOCKS5_VER, + rep => $reply, + rsv => 0, + atyp => $atyp + ); + $debug->add(hlen => $hlen) unless $resolve; + $debug->add( + bndaddr => $resolve ? _addr_ntoa($bndaddr, $atyp) : $bndaddr, + bndport => $port + ); + $debug->show('Server Send: '); + } + + 1; +} + +sub _socks4_accept_command { + my ($self, $ver) = @_; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my $resolve = defined(${*$self}->{SOCKS}->{Resolve}) ? ${*$self}->{SOCKS}->{Resolve} : $SOCKS4_RESOLVE; + my ($reads, $sends, $debugs) = (0, 0, 0); + + @{ ${*$self}->{SOCKS}->{COMMAND} } = (); + + #-------------------------------------------------------------------------- + # Read the auth mechanisms + #-------------------------------------------------------------------------- + # +-----+-----+----------+---------------+----------+------+ + # | VER | CMD | DST.PORT | DST.ADDR | USERID | NULL | + # +-----+-----+----------+---------------+----------+------+ + # | 1 | 1 | 2 | 4 | variable | 1 | + # +-----+-----+----------+---------------+----------+------+ + + my $request; + $request = $self->_socks_read($ver ? 7 : 8, ++$reads) + or return _fail($request); + + unless ($ver) { + $ver = unpack('C', $request); + substr($request, 0, 1) = ''; + } + + my ($cmd, $dstport) = unpack('Cn', $request); + substr($request, 0, 3) = ''; + my $dstaddr = length($request) == 4 ? inet_ntoa($request) : undef; + + my $userid = ''; + my $c; + + while (1) { + length($c = $self->_socks_read(1, ++$reads)) + or return _fail($c); + + if ($c ne "\0") { + $userid .= $c; + } + else { + last; + } + } + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => $ver, + cmd => $cmd, + dstport => $dstport, + dstaddr => $dstaddr, + userid => $userid, + null => 0 + ); + } + + my $atyp = ADDR_IPV4; + + if ($resolve && $dstaddr =~ /^0\.0\.0\.[1-9]/) { # socks4a + $dstaddr = ''; + $atyp = ADDR_DOMAINNAME; + + while (1) { + length($c = $self->_socks_read(1, ++$reads)) + or return _fail($c); + + if ($c ne "\0") { + $dstaddr .= $c; + } + else { + last; + } + } + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + dsthost => $dstaddr, + null => 0 + ); + } + } + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->show('Server Recv: '); + } + + if (defined(${*$self}->{SOCKS}->{UserAuth})) { + unless (&{ ${*$self}->{SOCKS}->{UserAuth} }($userid)) { + push @{${*$self}->{SOCKS}->{queue}}, [ + '_socks4_accept_command_reply', [ REQUEST_REJECTED_USERID, '0.0.0.0', 0 ], undef, [], 0, + sub { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(REQUEST_REJECTED_USERID, $@ = 'Authentication failed with SOCKS4 proxy'); + } + ]; + + return 0; + } + } + + if ($ver != SOCKS4_VER) { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(ISS_BAD_VERSION, $@ = "Socks version should be 4, $ver recieved"); + return; + } + + @{ ${*$self}->{SOCKS}->{COMMAND} } = ($cmd, $dstaddr, $dstport, $atyp); + + return 1; +} + +sub _socks4_accept_command_reply { + my $self = shift; + my $reply = shift; + my $host = shift; + my $port = shift; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my ($reads, $sends, $debugs) = (0, 0, 0); + + if (!defined($reply) || !defined($host) || !defined($port)) { + croak("You must provide a reply, host, and port on the command reply."); + } + + #-------------------------------------------------------------------------- + # Send the reply + #-------------------------------------------------------------------------- + # +-----+-----+----------+---------------+ + # | VER | REP | BND.PORT | BND.ADDR | + # +-----+-----+----------+---------------+ + # | 1 | 1 | 2 | 4 | + # +-----+-----+----------+---------------+ + + my $bndaddr = inet_aton($host) + or $SOCKS_ERROR->set(ISS_CANT_RESOLVE, $@ = "Can't resolve `$host'"), return; + my $rc; + $rc = $self->_socks_send(pack('CCna*', 0, $reply, $port, $bndaddr), ++$sends) + or return _fail($rc); + + if ($debug && !$self->_debugged(++$debugs)) { + $debug->add( + ver => 0, + rep => $reply, + bndport => $port, + bndaddr => length($bndaddr) == 4 ? inet_ntoa($bndaddr) : undef + ); + $debug->show('Server Send: '); + } + + 1; +} + +sub command { + my $self = shift; + + unless (exists ${*$self}->{SOCKS}->{RequireAuth}) # TODO: find more correct way + { + return ${*$self}->{SOCKS}->{COMMAND}; + } + else { + my @keys = qw(Version AuthType RequireAuth UserAuth Username Password + Debug Resolve AuthMethods CmdAddr CmdPort Bind TCP); + + my %tmp; + $tmp{$_} = ${*$self}->{SOCKS}->{$_} for @keys; + + my %args = @_; + $self->_configure(\%args); + + if ($self->_connect()) { + return 1; + } + + ${*$self}->{SOCKS}->{$_} = $tmp{$_} for @keys; + return 0; + } +} + +sub command_reply { + my $self = shift; + ${*$self}->{SOCKS}->{ready} = 0; + + if (${*$self}->{SOCKS}->{Version} == 4) { + ${*$self}->{SOCKS}->{queue} = [ [ '_socks4_accept_command_reply', [@_], undef, [], 0 ] ]; + } + else { + ${*$self}->{SOCKS}->{queue} = [ [ '_socks5_accept_command_reply', [@_], undef, [], 0 ] ]; + } + + $self->_run_queue(); +} + +sub dst { + my $self = shift; + my ($addr, $port, $atype) = @{ ${*$self}->{SOCKS} }{qw/DstAddr DstPort DstAddrType/}; + return (_addr_ntoa($addr, $atype), $port, $atype); +} + +sub send { + my $self = shift; + + unless (defined ${*$self}->{SOCKS}->{TCP}) { + return $self->SUPER::send(@_); + } + + my ($msg, $flags, $peer) = @_; + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + my $resolve = defined(${*$self}->{SOCKS}->{Resolve}) ? ${*$self}->{SOCKS}->{Resolve} : $SOCKS5_RESOLVE; + + croak "send: Cannot determine peer address" + unless defined $peer; + + my ($dstport, $dstaddr, $dstaddr_type); + if (ref $peer eq 'ARRAY') { + $dstaddr = $peer->[0]; + $dstport = $peer->[1]; + $dstaddr_type = ADDR_DOMAINNAME; + } + else { + unless (($dstport, $dstaddr, $dstaddr_type) = eval { (unpack_sockaddr_in($peer), ADDR_IPV4) }) { + ($dstport, $dstaddr, $dstaddr_type) = ((unpack_sockaddr_in6($peer))[ 0, 1 ], ADDR_IPV6); + } + } + + my ($sndaddr, $sndport, $sndaddr_type) = $self->dst; + if (($sndaddr eq '0.0.0.0' && $sndaddr_type == ADDR_IPV4) || ($sndaddr eq '::' && $sndaddr_type == ADDR_IPV6)) { + $sndaddr = ${*$self}->{SOCKS}->{ProxyAddr}; + $sndaddr_type = ADDR_DOMAINNAME; + } + if ($sndaddr_type == ADDR_DOMAINNAME) { + ($sndaddr_type, $sndaddr) = _resolve($sndaddr) + or $SOCKS_ERROR->set(ISS_CANT_RESOLVE, $@ = "Can't resolve `$sndaddr'"), return; + } + else { + $sndaddr = ${*$self}->{SOCKS}->{DstAddr}; + } + + $peer = $sndaddr_type == ADDR_IPV4 ? pack_sockaddr_in($sndport, $sndaddr) : pack_sockaddr_in6($sndport, $sndaddr); + + my $hlen; + if ($dstaddr_type == ADDR_DOMAINNAME) { + if ($resolve) { + $hlen = length $dstaddr; + } + else { + ($dstaddr_type, $dstaddr) = _resolve($dstaddr) + or $SOCKS_ERROR->set(ISS_CANT_RESOLVE, $@ = "Can't resolve `$dstaddr'"), return; + } + } + + my $msglen = $debug ? length($msg) : 0; + + # we need to add socks header to the message + # +----+------+------+----------+----------+----------+ + # |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | + # +----+------+------+----------+----------+----------+ + # | 2 | 1 | 1 | Variable | 2 | Variable | + # +----+------+------+----------+----------+----------+ + $msg = pack('C4', 0, 0, 0, $dstaddr_type) . (defined $hlen ? pack('C', $hlen) : '') . $dstaddr . pack('n', $dstport) . $msg; + + if ($debug) { + $debug->add( + rsv => '00', + frag => '0', + atyp => $dstaddr_type + ); + $debug->add(hlen => $hlen) if defined $hlen; + $debug->add( + dstaddr => defined $hlen ? $dstaddr : _addr_ntoa($dstaddr, $dstaddr_type), + dstport => $dstport, + data => "...($msglen)" + ); + $debug->show('Client Send: '); + } + + $self->SUPER::send($msg, $flags, $peer); +} + +sub recv { + my $self = shift; + + unless (defined ${*$self}->{SOCKS}->{TCP}) { + return $self->SUPER::recv(@_); + } + + my $debug = IO::Socket::Socks::Debug->new() if ${*$self}->{SOCKS}->{Debug}; + + defined($self->SUPER::recv($_[0], $_[1] + 262, $_[2])) + or return; + + # we need to remove socks header from the message + # +----+------+------+----------+----------+----------+ + # |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | + # +----+------+------+----------+----------+----------+ + # | 2 | 1 | 1 | Variable | 2 | Variable | + # +----+------+------+----------+----------+----------+ + my $rsv = join('', unpack('C2', $_[0])); + substr($_[0], 0, 2) = ''; + + my ($frag, $atyp) = unpack('C2', $_[0]); + substr($_[0], 0, 2) = ''; + + if ($debug) { + $debug->add( + rsv => $rsv, + frag => $frag, + atyp => $atyp + ); + } + + my $dstaddr; + if ($atyp == ADDR_DOMAINNAME) { + my $hlen = unpack('C', $_[0]); + $dstaddr = substr($_[0], 1, $hlen); + substr($_[0], 0, $hlen + 1) = ''; + + if ($debug) { + $debug->add(hlen => $hlen); + } + } + elsif ($atyp == ADDR_IPV4) { + $dstaddr = substr($_[0], 0, 4); + substr($_[0], 0, 4) = ''; + } + elsif ($atyp == ADDR_IPV6) { + $dstaddr = substr($_[0], 0, 16); + substr($_[0], 0, 16) = ''; + } + else { + $! = ESOCKSPROTO; + $SOCKS_ERROR->set(ISS_UNKNOWN_ADDRESS, $@ = "Unsupported address type returned by socks server: $atyp"); + return; + } + + my $dstport = unpack('n', $_[0]); + substr($_[0], 0, 2) = ''; + + if ($debug) { + $debug->add( + dstaddr => _addr_ntoa($dstaddr, $atyp), + dstport => $dstport, + data => "...(" . length($_[0]) . ")" + ); + $debug->show('Client Recv: '); + } + + return pack_sockaddr_in($dstport, $dstaddr) if $atyp == ADDR_IPV4; + return pack_sockaddr_in6($dstport, $dstaddr) if $atyp == ADDR_IPV6; + return [ $dstaddr, $dstport ]; +} + +#+----------------------------------------------------------------------------- +#| Helper Functions +#+----------------------------------------------------------------------------- +sub _socks_send { + my $self = shift; + my $data = shift; + my $numb = shift; + + local $SIG{PIPE} = 'IGNORE'; + $SOCKS_ERROR->set(); + + my $rc; + my $writed = 0; + my $blocking = ${*$self}{io_socket_timeout} ? $self->blocking(0) : $self->blocking; + + unless ($blocking || ${*$self}{io_socket_timeout}) { + if (${*$self}->{SOCKS}->{queue}[0][Q_SENDS] >= $numb) { # already sent + return 1; + } + + if (defined ${*$self}->{SOCKS}->{queue}[0][Q_BUF]) { # some chunk already sent + substr($data, 0, ${*$self}->{SOCKS}->{queue}[0][Q_BUF]) = ''; + } + + while (length $data) { + $rc = $self->syswrite($data); + if (defined $rc) { + if ($rc > 0) { + ${*$self}->{SOCKS}->{queue}[0][Q_BUF] += $rc; + substr($data, 0, $rc) = ''; + } + else { # XXX: socket closed? if smth writed, but not all? + last; + } + } + elsif ($! == EWOULDBLOCK || $! == EAGAIN) { + $SOCKS_ERROR->set(SOCKS_WANT_WRITE, 'Socks want write'); + return undef; + } + else { + $SOCKS_ERROR->set($!, $@ = "send: $!"); + last; + } + } + + $writed = int(${*$self}->{SOCKS}->{queue}[0][Q_BUF]); + ${*$self}->{SOCKS}->{queue}[0][Q_BUF] = undef; + ${*$self}->{SOCKS}->{queue}[0][Q_SENDS]++; + return $writed; + } + + my $selector = IO::Select->new($self); + my $start = time(); + + while (1) { + if (${*$self}{io_socket_timeout} && time() - $start >= ${*$self}{io_socket_timeout}) { + $! = ETIMEDOUT; + last; + } + + unless ($selector->can_write(1)) { # socket couldn't accept data for now, check if timeout expired and try again + next; + } + + $rc = $self->syswrite($data); + if ($rc > 0) { # reduce our message + $writed += $rc; + substr($data, 0, $rc) = ''; + if (length($data) == 0) { # all data successfully writed + last; + } + } + else { # some error in the socket; will return false + $SOCKS_ERROR->set($!, $@ = "send: $!") unless defined $rc; + last; + } + } + + $self->blocking(1) if $blocking; + + return $writed; +} + +sub _socks_read { + my $self = shift; + my $length = shift || 1; + my $numb = shift; + + $SOCKS_ERROR->set(); + my $data = ''; + my ($buf, $rc); + my $blocking = $self->blocking; + + # non-blocking read + unless ($blocking || ${*$self}{io_socket_timeout}) { # no timeout should be specified for non-blocking connect + if (defined ${*$self}->{SOCKS}->{queue}[0][Q_READS][$numb]) { # already readed + return ${*$self}->{SOCKS}->{queue}[0][Q_READS][$numb]; + } + + if (defined ${*$self}->{SOCKS}->{queue}[0][Q_BUF]) { # some chunk already readed + $data = ${*$self}->{SOCKS}->{queue}[0][Q_BUF]; + $length -= length $data; + } + + while ($length > 0) { + $rc = $self->sysread($buf, $length); + if (defined $rc) { + if ($rc > 0) { + $length -= $rc; + $data .= $buf; + } + else { # XXX: socket closed, if smth readed but not all? + last; + } + } + elsif ($! == EWOULDBLOCK || $! == EAGAIN) { # no data to read + if (length $data) { # save already readed data in the queue buffer + ${*$self}->{SOCKS}->{queue}[0][Q_BUF] = $data; + } + $SOCKS_ERROR->set(SOCKS_WANT_READ, 'Socks want read'); + return undef; + } + else { + $SOCKS_ERROR->set($!, $@ = "read: $!"); + last; + } + } + + ${*$self}->{SOCKS}->{queue}[0][Q_BUF] = undef; + ${*$self}->{SOCKS}->{queue}[0][Q_READS][$numb] = $data; + return $data; + } + + # blocking read + my $selector = IO::Select->new($self); + my $start = time(); + + while ($length > 0) { + if (${*$self}{io_socket_timeout} && time() - $start >= ${*$self}{io_socket_timeout}) { + $! = ETIMEDOUT; + last; + } + + unless ($selector->can_read(1)) { # no data in socket for now, check if timeout expired and try again + next; + } + + $rc = $self->sysread($buf, $length); + if (defined $rc && $rc > 0) { # reduce limit and modify buffer + $length -= $rc; + $data .= $buf; + } + else { # EOF or error in the socket + $SOCKS_ERROR->set($!, $@ = "read: $!") unless defined $rc; + last; # TODO handle unexpected EOF more correct + } + } + + # XXX it may return incomplete $data if timed out. Could it break smth? + return $data; +} + +sub _debugged { + my ($self, $debugs) = @_; + + if (${*$self}->{SOCKS}->{queue}[0][Q_DEBUGS] >= $debugs) { + return 1; + } + + ${*$self}->{SOCKS}->{queue}[0][Q_DEBUGS] = $debugs; + return 0; +} + +sub _fail { + if (!@_ || defined($_[0])) { + $SOCKS_ERROR->set(ECONNABORTED, $@ = 'Socket closed by remote side') if $SOCKS_ERROR == undef; + return; + } + + return -1; +} + +sub _validate_multi_version { + my $multi_ver = shift; + + if (@$multi_ver == 1) { + return $multi_ver->[0] == 4 || $multi_ver->[0] == 5; + } + + if (@$multi_ver == 2) { + return + $multi_ver->[0] != $multi_ver->[1] + && ($multi_ver->[0] == 4 || $multi_ver->[0] == 5) + && ($multi_ver->[1] == 4 || $multi_ver->[1] == 5); + } + + return; +} + +sub _resolve { + my $addr = shift; + my ($err, @res) = Socket::getaddrinfo($addr, undef, { protocol => Socket::IPPROTO_TCP, socktype => Socket::SOCK_STREAM }); + return if $err; + + for my $r (@res) { + if ($r->{family} == PF_INET) { + return (ADDR_IPV4, (unpack_sockaddr_in($r->{addr}))[1]); + } + } + + return (ADDR_IPV6, (unpack_sockaddr_in6($res[0]{addr}))[1]); +} + +sub _addr_ntoa { + my ($addr, $atype) = @_; + + return inet_ntoa($addr) if ($atype == ADDR_IPV4); + return Socket::inet_ntop(AF_INET6, $addr) if ($atype == ADDR_IPV6); + return $addr; +} + +############################################################################### +#+----------------------------------------------------------------------------- +#| Helper Package to bring some magic in $SOCKS_ERROR +#+----------------------------------------------------------------------------- +############################################################################### + +package IO::Socket::Socks::Error; + +use overload + '==' => \&num_eq, + '!=' => sub { !num_eq(@_) }, + '""' => \&as_str, + '0+' => \&as_num; + +sub new { + my ($class, $num, $str) = @_; + + my $self = { + num => $num, + str => $str, + }; + + bless $self, $class; +} + +sub set { + my ($self, $num, $str) = @_; + + $self->{num} = defined $num ? int($num) : $num; + $self->{str} = $str; +} + +sub as_str { + my $self = shift; + return $self->{str}; +} + +sub as_num { + my $self = shift; + return $self->{num}; +} + +sub num_eq { + my ($self, $num) = @_; + + unless (defined $num) { + return !defined($self->{num}); + } + return $self->{num} == int($num); +} + +############################################################################### +#+----------------------------------------------------------------------------- +#| Helper Package to prevent modifications of $SOCKS_ERROR outside this package +#+----------------------------------------------------------------------------- +############################################################################### + +package IO::Socket::Socks::ReadOnlyVar; + +sub TIESCALAR { + my ($class, $value) = @_; + bless \$value, $class; +} + +sub FETCH { + my $self = shift; + return $$self; +} + +*STORE = *UNTIE = sub { Carp::croak 'Modification of readonly value attempted' }; + +############################################################################### +#+----------------------------------------------------------------------------- +#| Helper Package to handle assigning of $SOCKET_CLASS +#+----------------------------------------------------------------------------- +############################################################################### + +package IO::Socket::Socks::SocketClassVar; + +sub TIESCALAR { + my ($class, $value) = @_; + bless { v => $value }, $class; +} + +sub FETCH { + return $_[0]->{v}; +} + +sub STORE { + my ($self, $class) = @_; + + $self->{v} = $class; + eval "use $class; 1" or die $@; + $IO::Socket::Socks::ISA[1] = $class; +} + +sub UNTIE { + Carp::croak 'Untie of tied variable is denied'; +} + +############################################################################### +#+----------------------------------------------------------------------------- +#| Helper Package to display pretty debug messages +#+----------------------------------------------------------------------------- +############################################################################### + +package IO::Socket::Socks::Debug; + +sub new { + my ($class) = @_; + my $self = []; + + bless $self, $class; +} + +sub add { + my $self = shift; + push @{$self}, @_; +} + +sub show { + my ($self, $tag) = @_; + + $self->_separator($tag); + $self->_row(0, $tag); + $self->_separator($tag); + $self->_row(1, $tag); + $self->_separator($tag); + + print STDERR "\n"; + + @{$self} = (); +} + +sub _separator { + my $self = shift; + my $tag = shift; + my ($row1_len, $row2_len, $len); + + print STDERR $tag, '+'; + + for (my $i = 0 ; $i < @$self ; $i += 2) { + $row1_len = length($self->[$i]); + $row2_len = length($self->[ $i + 1 ]); + $len = ($row1_len > $row2_len ? $row1_len : $row2_len) + 2; + + print STDERR '-' x $len, '+'; + } + + print STDERR "\n"; +} + +sub _row { + my $self = shift; + my $row = shift; + my $tag = shift; + my ($row1_len, $row2_len, $len); + + print STDERR $tag, '|'; + + for (my $i = 0 ; $i < @$self ; $i += 2) { + $row1_len = length($self->[$i]); + $row2_len = length($self->[ $i + 1 ]); + $len = ($row1_len > $row2_len ? $row1_len : $row2_len); + + printf STDERR ' %-' . $len . 's |', $self->[ $i + $row ]; + } + + print STDERR "\n"; +} + +1; + +__END__ + +=head1 NAME + +IO::Socket::Socks - Provides a way to create socks client or server both 4 and 5 version. + +=head1 SYNOPSIS + +=head2 Client + + use IO::Socket::Socks; + + my $socks_client = IO::Socket::Socks->new( + ProxyAddr => "proxy host", + ProxyPort => "proxy port", + ConnectAddr => "remote host", + ConnectPort => "remote port", + ) or die $SOCKS_ERROR; + + print $socks_client "foo\n"; + $socks_client->close(); + +=head2 Server + + use IO::Socket::Socks ':constants'; + + my $socks_server = IO::Socket::Socks->new( + ProxyAddr => "localhost", + ProxyPort => 8000, + Listen => 1, + UserAuth => \&auth, + RequireAuth => 1 + ) or die $SOCKS_ERROR; + + while(1) { + my $client = $socks_server->accept(); + + unless ($client) { + print "ERROR: $SOCKS_ERROR\n"; + next; + } + + my $command = $client->command(); + if ($command->[0] == CMD_CONNECT) { + # Handle the CONNECT + $client->command_reply(REPLY_SUCCESS, addr, port); + } + + ... + #read from the client and send to the CONNECT address + ... + + $client->close(); + } + + sub auth { + my ($user, $pass) = @_; + + return 1 if $user eq "foo" && $pass eq "bar"; + return 0; + } + +=head1 DESCRIPTION + +C connects to a SOCKS proxy, tells it to open a +connection to a remote host/port when the object is created. The +object you receive can be used directly as a socket (with C interface) +for sending and receiving data from the remote host. In addition to create socks client +this module could be used to create socks server. See examples below. + +=head1 EXAMPLES + +For complete examples of socks 4/5 client and server see `examples' +subdirectory in the distribution. + +=head1 METHODS + +=head2 Socks Client + +=head3 new( %cfg ) + +=head3 new_from_socket($socket, %cfg) + +=head3 new_from_fd($socket, %cfg) + +Creates a new IO::Socket::Socks client object. new_from_socket() is the same as +new(), but allows one to create object from an existing and not connected socket +(new_from_fd is new_from_socket alias). To make IO::Socket::Socks object from +connected socket see C + +Both takes the following config hash: + + SocksVersion => 4 or 5. Default is 5 + + Timeout => connect/accept timeout + + Blocking => Since IO::Socket::Socks version 0.5 you can perform non-blocking connect/bind by + passing false value for this option. Default is true - blocking. See ready() + below for more details. + + SocksResolve => resolve host name to ip by proxy server or + not (will resolve by client). This + overrides value of $SOCKS4_RESOLVE or $SOCKS5_RESOLVE + variable. Boolean. + + SocksDebug => This will cause all of the SOCKS traffic to + be presented on the command line in a form + similar to the tables in the RFCs. This overrides value + of $SOCKS_DEBUG variable. Boolean. + + ProxyAddr => Hostname of the proxy + + ProxyPort => Port of the proxy + + ConnectAddr => Hostname of the remote machine + + ConnectPort => Port of the remote machine + + BindAddr => Hostname of the remote machine which will + connect to the proxy server after bind request + + BindPort => Port of the remote machine which will + connect to the proxy server after bind request + + UdpAddr => Expected address where datagrams will be sent. Fill it with address + of all zeros if address is not known at this moment. + Proxy server may use this information to limit access to the association. + + UdpPort => Expected port where datagrams will be sent. Use zero port + if port is not known at this moment. Proxy server may use this + information to limit access to the association. + + AuthType => What kind of authentication to support: + none - no authentication (default) + userpass - Username/Password. For socks5 + proxy only. + + RequireAuth => Do not send ANON as a valid auth mechanism. + For socks5 proxy only + + Username => For socks5 if AuthType is set to userpass, then + you must provide a username. For socks4 proxy with + this option you can specify userid. + + Password => If AuthType is set to userpass, then you must + provide a password. For socks5 proxy only. + +The following options should be specified: + + (ProxyAddr and ProxyPort) + (ConnectAddr and ConnectPort) or (BindAddr and BindPort) or (UdpAddr and UdpPort) + +Other options are facultative. + +=head3 +start_SOCKS($socket, %cfg) + +This is a class method to start socks handshake on already connected socket. This +will bless passed $socket to IO::Socket::Socks class. %cfg is like hash in the constructor. +Only options listed below makes sence: + + Timeout + ConnectAddr + ConnectPort + BindAddr + BindPort + UdpAddr + UdpPort + SocksVersion + SocksDebug + SocksResolve + AuthType + RequireAuth + Username + Password + AuthMethods + +On success this method will return same $socket, but as IO::Socket::Socks object. On failure it will +return undef (but socket will be still blessed to IO::Socket::Socks class). See example: + + use IO::Socket; + use IO::Socket::Socks; + + my $sock = IO::Socket::INET->new("$proxy_host:$proxy_port") or die $@; + $sock = IO::Socket::Socks->start_SOCKS($sock, ConnectAddr => "google.com", ConnectPort => 80) or die $SOCKS_ERROR; + +=head3 +version( ) + +Returns socks version for this socket + +=head3 +ready( ) + +Returns true when socket becomes ready to transfer data (socks handshake done), +false otherwise. This is useful for non-blocking connect/bind. When this method +returns false value you can determine what socks handshake need for with $SOCKS_ERROR +variable. It may need for read, then $SOCKS_ERROR will be SOCKS_WANT_READ or need for +write, then it will be SOCKS_WANT_WRITE. + +Example: + + use IO::Socket::Socks; + use IO::Select; + + my $sock = IO::Socket::Socks->new( + ProxyAddr => 'localhost', ProxyPort => 1080, ConnectAddr => 'mail.com', ConnectPort => 80, Blocking => 0 + ) or die $SOCKS_ERROR; + + my $sel = IO::Select->new($sock); + until ($sock->ready) { + if ($SOCKS_ERROR == SOCKS_WANT_READ) { + $sel->can_read(); + } + elsif ($SOCKS_ERROR == SOCKS_WANT_WRITE) { + $sel->can_write(); + } + else { + die $SOCKS_ERROR; + } + + # NOTE: when base class ($IO::Socket::Socks::SOCKET_CLASS) is IO::Socket::IP + # and you are using kqueue or epoll to check for readable/writable sockets + # you need to readd $sock to kqueue/epoll after each call to ready() (actually until socket will be connected to proxy server), + # because IO::Socket::IP may change internal socket of $sock for milti-homed hosts. + # There is no such problem when you are using select/poll + } + + # you may want to return socket to blocking state by $sock->blocking(1) + $sock->syswrite("I am ready"); + +=head3 +accept( ) + +Accept an incoming connection after bind request. On failed returns undef. +On success returns socket. No new socket created, returned socket is same +on which this method was called. Because accept(2) is not invoked on the +client side, socks server calls accept(2) and proxify all traffic via socket +opened by client bind request. You can call accept only once on IO::Socket::Socks +client socket. + +=head3 +command( %cfg ) + +Allows one to execute socks command on already opened socket. Thus you +can create socks chain. For example see L section. + +%cfg is like hash in the constructor. Only options listed below makes sence: + + ConnectAddr + ConnectPort + BindAddr + BindPort + UdpAddr + UdpPort + SocksVersion + SocksDebug + SocksResolve + AuthType + RequireAuth + Username + Password + AuthMethods + +Values of the other options (Timeout for example) inherited from the constructor. +Options like ProxyAddr and ProxyPort are not included. + +=head3 +dst( ) + +Return (host, port, address_type) of the remote host after connect/accept or socks server (host, port, address_type) +after bind/udpassoc. + +=head2 Socks Server + +=head3 new( %cfg ) + +=head3 new_from_socket($socket, %cfg) + +=head3 new_from_fd($socket, %cfg) + +Creates a new IO::Socket::Socks server object. new_from_socket() is the same as +new(), but allows one to create object from an existing socket (new_from_fd is new_from_socket alias). +Both takes the following config hash: + + SocksVersion => 4 for socks4, 5 for socks5 or [4,5] if you want accept both 4 and 5. Default is 5 + + Timeout => Timeout value for various operations + + Blocking => Since IO::Socket::Socks version 0.6 you can perform non-blocking accept by + passing false value for this option. Default is true - blocking. See ready() + below for more details. + + SocksResolve => For socks v5: return destination address to the client + in form of 4 bytes if true, otherwise in form of host + length and host name. + For socks v4: allow use socks4a protocol extension if + true and not otherwise. + This overrides value of $SOCKS4_RESOLVE or $SOCKS5_RESOLVE. + See also command_reply(). + + SocksDebug => This will cause all of the SOCKS traffic to + be presented on the command line in a form + similar to the tables in the RFCs. This overrides value + of $SOCKS_DEBUG variable. Boolean. + + ProxyAddr => Local host bind address + + ProxyPort => Local host bind port + + UserAuth => Reference to a function that returns 1 if client + allowed to use socks server, 0 otherwise. For + socks5 proxy it takes login and password as + arguments. For socks4 argument is userid. + + RequireAuth => Not allow anonymous access for socks5 proxy. + + Listen => Same as IO::Socket::INET listen option. Should be + specified as number > 0. + +The following options should be specified: + + Listen + ProxyAddr + ProxyPort + +Other options are facultative. + +=head3 accept( ) + +Accept an incoming connection and return a new IO::Socket::Socks +object that represents that connection. You must call command() +on this to find out what the incoming connection wants you to do, +and then call command_reply() to send back the reply. + +=head3 version( ) + +Returns socks version for socket. It is useful when your server +accepts both 4 and 5 version. Then you should know socks version +to make proper response. Just call C on socket received +after C. + +=head3 ready( ) + +After non-blocking accept you will get new client socket object, which may be +not ready to transfer data (if socks handshake is not done yet). ready() will return +true value when handshake will be done successfully and false otherwise. Note, socket +returned by accept() call will be always in blocking mode. So if your program can't +block you should set non-blocking mode for this socket before ready() call: $socket->blocking(0). +When ready() returns false value you can determine what socks handshake needs for with $SOCKS_ERROR +variable. It may need for read, then $SOCKS_ERROR will be SOCKS_WANT_READ or need for +write, then it will be SOCKS_WANT_WRITE. + +Example: + + use IO::Socket::Socks; + use IO::Select; + + my $server = IO::Socket::Socks->new(ProxyAddr => 'localhost', ProxyPort => 1080, Blocking => 0) + or die $@; + my $select = IO::Select->new($server); + $select->can_read(); # wait for client + + my $client = $server->accept() + or die "accept(): $! ($SOCKS_ERROR)"; + $client->blocking(0); # !!! + $select->add($client); + $select->remove($server); # no more connections + + while (1) { + if ($client->ready) { + my $command = $client->command; + + ... # do client command + + $client->command_reply(IO::Socket::Socks::REPLY_SUCCESS, $command->[1], $command->[2]); + + ... # transfer traffic + + last; + } + elsif ($SOCKS_ERROR == SOCKS_WANT_READ) { + $select->can_read(); + } + elsif ($SOCKS_ERROR == SOCKS_WANT_WRITE) { + $select->can_write(); + } + else { + die "Unexpected error: $SOCKS_ERROR"; + } + } + +=head3 command( ) + +After you call accept() the client has sent the command they want +you to process. This function should be called on the socket returned +by accept(). It returns a reference to an array with the following format: + + [ COMMAND, ADDRESS, PORT, ADDRESS TYPE ] + +=head3 command_reply( REPLY CODE, ADDRESS, PORT ) + +After you call command() the client needs to be told what the result +is. The REPLY CODE is one of the constants as follows (integer value): + + For socks v4 + REQUEST_GRANTED(90): request granted + REQUEST_FAILED(91): request rejected or failed + REQUEST_REJECTED_IDENTD(92): request rejected because SOCKS server cannot connect to identd on the client + REQUEST_REJECTED_USERID(93): request rejected because the client program and identd report different user-ids + + For socks v5 + REPLY_SUCCESS(0): Success + REPLY_GENERAL_FAILURE(1): General Failure + REPLY_CONN_NOT_ALLOWED(2): Connection Not Allowed + REPLY_NETWORK_UNREACHABLE(3): Network Unreachable + REPLY_HOST_UNREACHABLE(4): Host Unreachable + REPLY_CONN_REFUSED(5): Connection Refused + REPLY_TTL_EXPIRED(6): TTL Expired + REPLY_CMD_NOT_SUPPORTED(7): Command Not Supported + REPLY_ADDR_NOT_SUPPORTED(8): Address Not Supported + +HOST and PORT are the resulting host and port (where server socket responsible for this command bound). + +Note: for 5 version C will try to resolve passed address if +C has true value and passed address is domain name. To avoid this just pass ip address +(C<$socket-Esockhost>) instead of host name or turn off C for this server. For version 4 +passed host name will always be resolved to ip address even if C has false value. Because +this version doesn't support C
as domain name. + +=head1 VARIABLES + +=head2 $SOCKS_ERROR + +This scalar behaves like $! in that if undef is returned. C<$SOCKS_ERROR> is IO::Socket::Socks::Error +object with some overloaded operators. In string context this variable should contain a string reason for +the error. In numeric context it contains error code. + +=head2 $SOCKS4_RESOLVE + +If this variable has true value resolving of host names will be done +by proxy server, otherwise resolving will be done locally. Resolving +host by socks proxy version 4 is extension to the protocol also known +as socks4a. So, only socks4a proxy supports resolving of hostnames. +Default value of this variable is false. This variable is not importable. +See also `SocksResolve' parameter in the constructor. + +=head2 $SOCKS5_RESOLVE + +If this variable has true value resolving of host names will be done +by proxy server, otherwise resolving will be done locally. Note: some +bugous socks5 servers doesn't support resolving of host names. Default +value is true. This variable is not importable. +See also `SocksResolve' parameter in the constructor. + +=head2 $SOCKS_DEBUG + +Default value is $ENV{SOCKS_DEBUG}. If this variable has true value and +no SocksDebug option in the constructor specified, then SocksDebug will +has true value. This variable is not importable. + +=head2 $SOCKET_CLASS + +With this variable you can get/set base socket class for C. +By default it tries to use C 0.36+ as socket class. And falls +back to C if not available. You can set C<$IO::Socket::Socks::SOCKET_CLASS> +before loading of C and then it will not try to detect proper base class +itself. You can also set it after loading of C and this will automatically +update C<@ISA>, so you shouldn't worry about inheritance. + +=head1 CONSTANTS + +The following constants could be imported manually or using `:constants' tag: + + SOCKS5_VER + SOCKS4_VER + ADDR_IPV4 + ADDR_DOMAINNAME + ADDR_IPV6 + CMD_CONNECT + CMD_BIND + CMD_UDPASSOC + AUTHMECH_ANON + AUTHMECH_USERPASS + AUTHMECH_INVALID + AUTHREPLY_SUCCESS + AUTHREPLY_FAILURE + ISS_UNKNOWN_ADDRESS # address type sent by client/server not supported by I::S::S + ISS_BAD_VERSION # socks version sent by client/server != specified version + ISS_CANT_RESOLVE # I::S::S failed to resolve some host + REPLY_SUCCESS + REPLY_GENERAL_FAILURE + REPLY_CONN_NOT_ALLOWED + REPLY_NETWORK_UNREACHABLE + REPLY_HOST_UNREACHABLE + REPLY_CONN_REFUSED + REPLY_TTL_EXPIRED + REPLY_CMD_NOT_SUPPORTED + REPLY_ADDR_NOT_SUPPORTED + REQUEST_GRANTED + REQUEST_FAILED + REQUEST_REJECTED_IDENTD + REQUEST_REJECTED_USERID + SOCKS_WANT_READ + SOCKS_WANT_WRITE + ESOCKSPROTO + +SOCKS_WANT_READ, SOCKS_WANT_WRITE and ESOCKSPROTO are imported by default. + +=head1 IPv6 + +Since version 0.66 C supports IPv6 with help of L +0.36+. And will use C as base class if available. However you can +force set C<$SOCKET_CLASS = "IO::Socket::INET"> to use IPv4 only. See also +L + +=head1 FAQ + +=over + +=item How to determine is connection to socks server (client accept) failed or some protocol error +occurred? + +You can check $! variable. If $! == ESOCKSPROTO constant, then it was error in the protocol. Error +description could be found in $SOCKS_ERROR. + +=item How to determine which error in the protocol occurred? + +You should compare C<$SOCKS_ERROR> with constants below: + + AUTHMECH_INVALID + AUTHREPLY_FAILURE + ISS_UNKNOWN_ADDRESS + ISS_BAD_VERSION + REPLY_GENERAL_FAILURE + REPLY_CONN_NOT_ALLOWED + REPLY_NETWORK_UNREACHABLE + REPLY_HOST_UNREACHABLE + REPLY_CONN_REFUSED + REPLY_TTL_EXPIRED + REPLY_CMD_NOT_SUPPORTED + REPLY_ADDR_NOT_SUPPORTED + REQUEST_FAILED + REQUEST_REJECTED_IDENTD + REQUEST_REJECTED_USERID + +=back + +=head1 BUGS + +The following options are not implemented: + +=over + +=item GSSAPI authentication + +=item UDP server side support + +=back + +Patches are welcome. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Original author is Ryan Eatmon + +Now maintained by Oleg G + +=head1 COPYRIGHT + +This module is free software, you can redistribute it and/or modify +it under the terms of LGPL. + +=cut diff --git a/CPAN/Net/HTTPS/NB.pm b/CPAN/Net/HTTPS/NB.pm new file mode 100644 index 00000000000..2c5740b9a47 --- /dev/null +++ b/CPAN/Net/HTTPS/NB.pm @@ -0,0 +1,311 @@ +package Net::HTTPS::NB; + +use strict; +use Net::HTTP; +use IO::Socket::SSL 0.98; +use Exporter; +use Errno qw(EWOULDBLOCK EAGAIN); +use vars qw($VERSION @ISA @EXPORT $HTTPS_ERROR); + +$VERSION = 0.15; + +=head1 NAME + +Net::HTTPS::NB - Non-blocking HTTPS client + +=head1 SYNOPSIS + +=over + +=item Example of sending request and receiving response + + use strict; + use Net::HTTPS::NB; + use IO::Select; + use Errno qw/EAGAIN EWOULDBLOCK/; + + my $s = Net::HTTPS::NB->new(Host => "pause.perl.org") || die $@; + $s->write_request(GET => "/"); + + my $sel = IO::Select->new($s); + + READ_HEADER: { + die "Header timeout" unless $sel->can_read(10); + my($code, $mess, %h) = $s->read_response_headers; + redo READ_HEADER unless $code; + } + + # Net::HTTPS::NB uses internal buffer for reading + # so we should check it before socket check by calling read_entity_body() + # it is error to wait data on socket before read_entity_body() will return undef + # with $! set to EAGAIN or EWOULDBLOCK + # make socket non-blocking, so read_entity_body() will not block + $s->blocking(0); + + while (1) { + my $buf; + my $n; + # try to read until error or all data received + while (1) { + my $tmp_buf; + $n = $s->read_entity_body($tmp_buf, 1024); + if ($n == -1 || (!defined($n) && ($! == EWOULDBLOCK || $! == EAGAIN))) { + last; # no data available this time + } + elsif ($n) { + $buf .= $tmp_buf; # data received + } + elsif (defined $n) { + last; # $n == 0, all readed + } + else { + die "Read error occured: ", $!; # $n == undef + } + } + + print $buf if length $buf; + last if defined $n && $n == 0; # all readed + die "Body timeout" unless $sel->can_read(10); # wait for new data + } + +=item Example of non-blocking connect + + use strict; + use Net::HTTPS::NB; + use IO::Select; + + my $sock = Net::HTTPS::NB->new(Host => 'encrypted.google.com', Blocking => 0); + my $sele = IO::Select->new($sock); + + until ($sock->connected) { + if ($HTTPS_ERROR == HTTPS_WANT_READ) { + $sele->can_read(); + } + elsif($HTTPS_ERROR == HTTPS_WANT_WRITE) { + $sele->can_write(); + } + else { + die 'Unknown error: ', $HTTPS_ERROR; + } + } + +=back + +See `examples' subdirectory for more examples. + +=head1 DESCRIPTION + +Same interface as Net::HTTPS but it will never try multiple reads when the +read_response_headers() or read_entity_body() methods are invoked. In addition +allows non-blocking connect. + +=over + +=item If read_response_headers() did not see enough data to complete the headers an empty list is returned. + +=item If read_entity_body() did not see new entity data in its read the value -1 is returned. + +=back + +=cut + +# we only supports IO::Socket::SSL now +# use it force +$Net::HTTPS::SSL_SOCKET_CLASS = 'IO::Socket::SSL'; +require Net::HTTPS; + +# make aliases to IO::Socket::SSL variables and constants +use constant { + HTTPS_WANT_READ => SSL_WANT_READ, + HTTPS_WANT_WRITE => SSL_WANT_WRITE, +}; +*HTTPS_ERROR = \$SSL_ERROR; + +=head1 PACKAGE CONSTANTS + +Imported by default + + HTTPS_WANT_READ + HTTPS_WANT_WRITE + +=head1 PACKAGE VARIABLES + +Imported by default + + $HTTPS_ERROR + +=cut + +# need export some stuff for error handling +@EXPORT = qw($HTTPS_ERROR HTTPS_WANT_READ HTTPS_WANT_WRITE); +@ISA = qw(Net::HTTPS Exporter); + +=head1 METHODS + +=head2 new(%cfg) + +Same as Net::HTTPS::new, but in addition allows `Blocking' parameter. By setting +this parameter to 0 you can perform non-blocking connect. See connected() to +determine when connection completed. + +=cut + +sub new { + my ($class, %args) = @_; + + my %ssl_opts; + while (my $name = each %args) { + if (substr($name, 0, 4) eq 'SSL_') { + $ssl_opts{$name} = delete $args{$name}; + } + } + + unless (exists $args{PeerPort}) { + $args{PeerPort} = 443; + } + + # create plain socket first + my $self = Net::HTTP->new(%args) + or return; + + # and upgrade it to SSL then for SNI + $class->start_SSL($self, %ssl_opts, SSL_startHandshake => 0, PeerHost => $args{Host}) + or return; + + if (!exists($args{Blocking}) || $args{Blocking}) { + # blocking connect + $self->connected() + or return; + } + # non-blocking handshake will be started after plain socket connected + + return $self; +} + +=head2 connected() + +Returns true value when connection completed (https handshake done). Otherwise +returns false. In this case you can check $HTTPS_ERROR to determine what handshake +need for, read or write. $HTTPS_ERROR could be HTTPS_WANT_READ or HTTPS_WANT_WRITE +respectively. See L. + +=cut + +sub connected { + my $self = shift; + + if (exists ${*$self}{httpsnb_connected}) { + # already connected or disconnected + return ${*$self}{httpsnb_connected} && getpeername($self); + } + + if ($self->connect_SSL()) { + return ${*$self}{httpsnb_connected} = 1; + } + elsif ($! != EWOULDBLOCK && $! != EAGAIN) { + $HTTPS_ERROR = $!; + } + return 0; +} + +sub close { + my $self = shift; + # need some cleanup + ${*$self}{httpsnb_connected} = 0; + return $self->SUPER::close(); +} + +=head2 blocking($flag) + +As opposed to Net::HTTPS where blocking method consciously broken you +can set socket blocking. For example you can return socket to blocking state +after non-blocking connect. + +=cut + +sub blocking { + # blocking() is breaked in Net::HTTPS + # restore it here + my $self = shift; + $self->IO::Socket::blocking(@_); +} + +# code below copied from Net::HTTP::NB with some modifications +# Author: Gisle Aas + +sub sysread { + my $self = shift; + unless (${*$self}{'httpsnb_reading'}) { + # allow reading without restrictions when called + # not from our methods + return $self->SUPER::sysread(@_); + } + + if (${*$self}{'httpsnb_read_count'}++) { + ${*$self}{'http_buf'} = ${*$self}{'httpsnb_save'}; + die "Multi-read\n"; + } + + my $offset = $_[2] || 0; + my $n = $self->SUPER::sysread($_[0], $_[1], $offset); + ${*$self}{'httpsnb_save'} .= substr($_[0], $offset); + return $n; +} + +sub read_response_headers { + my $self = shift; + ${*$self}{'httpsnb_reading'} = 1; + ${*$self}{'httpsnb_read_count'} = 0; + ${*$self}{'httpsnb_save'} = ${*$self}{'http_buf'}; + my @h = eval { $self->SUPER::read_response_headers(@_) }; + ${*$self}{'httpsnb_reading'} = 0; + if ($@) { + return if $@ eq "Multi-read\n" || $HTTPS_ERROR == HTTPS_WANT_READ; + die; + } + return @h; +} + +sub read_entity_body { + my $self = shift; + ${*$self}{'httpsnb_reading'} = 1; + ${*$self}{'httpsnb_read_count'} = 0; + ${*$self}{'httpsnb_save'} = ${*$self}{'http_buf'}; + + my $chunked = ${*$self}{'http_chunked'}; + my $bytes = ${*$self}{'http_bytes'}; + my $first_body = ${*$self}{'http_first_body'}; + my @request_method = @{${*$self}{'http_request_method'}}; + + # XXX I'm not so sure this does the correct thing in case of + # transfer-encoding tranforms + my $n = eval { $self->SUPER::read_entity_body(@_) }; + ${*$self}{'httpsnb_reading'} = 0; + if ($@ || (!defined($n) && $HTTPS_ERROR == HTTPS_WANT_READ)) { + if ($@ eq "Multi-read\n") { + # Reset some internals of Net::HTTP::Methods + ${*$self}{'http_chunked'} = $chunked; + ${*$self}{'http_bytes'} = $bytes; + ${*$self}{'http_first_body'} = $first_body; + ${*$self}{'http_request_method'} = \@request_method; + } + $_[0] = ""; + return -1; + } + return $n; +} + +1; + +=head1 SEE ALSO + +L, L, L + +=head1 COPYRIGHT + +Copyright 2011-2015 Oleg G . + +This library is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +=cut diff --git a/CPAN/Network/IPv4Addr.pm b/CPAN/Net/IPv4Addr.pm similarity index 83% rename from CPAN/Network/IPv4Addr.pm rename to CPAN/Net/IPv4Addr.pm index 15a0ea78a94..31bd95eddc0 100644 --- a/CPAN/Network/IPv4Addr.pm +++ b/CPAN/Net/IPv4Addr.pm @@ -1,384 +1,385 @@ -# IPv4Addr.pm - Perl module to manipulate IPv4 addresses. -# -# Author: Francis J. Lacoste -# -# Copyright (C) 1999 Francis J. Lacoste, iNsu Innovations Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms as perl itself. -# - -package Network::IPv4Addr; - -use strict; -use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); - -require Exporter; -require AutoLoader; - -@ISA = qw(Exporter AutoLoader); - -# Items to export into callers namespace by default. Note: do not export -# names by default without a very good reason. Use EXPORT_OK instead. -# Do not simply export all your public functions/methods/constants. -@EXPORT = qw(); - -%EXPORT_TAGS = ( - all => [qw{ ipv4_parse ipv4_checkip - ipv4_network ipv4_broadcast - ipv4_cidr2msk ipv4_msk2cidr - ipv4_in_network ipv4_dflt_netmask - } ], - ); - -@EXPORT_OK = qw(); - -Exporter::export_ok_tags('all'); - -$VERSION = '0.05'; - -# Preloaded methods go here. -use Carp; - -# Functions to manipulate IPV4 address -my $ip_rgx = "\\d+\\.\\d+\\.\\d+\\.\\d+"; - -# Given an IPv4 address in host, ip/netmask or cidr format -# returns a ip / cidr pair. -sub ipv4_parse($;$) { - my ($ip,$msk); - # Called with 2 args, assume first is IP address - if ( defined $_[1] ) { - $ip = $_[0]; - $msk= $_[1]; - } else { - ($ip) = $_[0] =~ /($ip_rgx)/o; - ($msk) = $_[0] =~ m!/(.+)!o; - } - - # Remove white spaces - $ip = ipv4_chkip( $ip ) or - croak __PACKAGE__, ": invalid IPv4 address: ", $ip, "\n"; - $msk =~ s/\s//g if defined $msk; - - # Check Netmask to see if it is a CIDR or Network - if (defined $msk ) { - if ($msk =~ /^\d{1,2}$/) { - # Check cidr - croak __PACKAGE__, ": invalid cidr: ", $msk, "\n" - if $msk < 0 or $msk > 32; - } elsif ($msk =~ /^$ip_rgx$/o ) { - $msk = ipv4_msk2cidr($msk); - } else { - croak __PACKAGE__, ": invalid netmask specification: ", $msk, "\n"; - } - } else { - # Host - return $ip; - } - wantarray ? ($ip,$msk) : "$ip/$msk"; -} - -sub ipv4_dflt_netmask($) { - my ($ip) = ipv4_parse($_[0]); - - my ($b1) = split /\./, $ip; - - return "255.0.0.0" if $b1 <= 127; - return "255.255.0.0" if $b1 <= 191; - return "255.255.255.0"; -} - -# Check form a valid IPv4 address. -sub ipv4_chkip($) { - my ($ip) = $_[0] =~ /($ip_rgx)/o; - - return undef unless $ip; - - # Check that bytes are in range - for (split /\./, $ip ) { - return undef if $_ < 0 or $_ > 255; - } - return $ip; -} - -# Transform a netmask in a CIDR mask length -sub ipv4_msk2cidr($) { - my $msk = ipv4_chkip( $_[0] ) - or croak __PACKAGE__, ": invalid netmask: ", $_[0], "\n"; - - my @bytes = split /\./, $msk; - - my $cidr = 0; - for (@bytes) { - my $bits = unpack( "B*", pack( "C", $_ ) ); - $cidr += $bits =~ tr /1/1/; - } - return $cidr; -} - -# Transform a CIDR mask length in a netmask -sub ipv4_cidr2msk($) { - my $cidr = shift; - croak __PACKAGE__, ": invalid cidr: ", $cidr, "\n" - if $cidr < 0 or $cidr > 32; - - my $bits = "1" x $cidr . "0" x (32 - $cidr); - - return join ".", (unpack 'CCCC', pack("B*", $bits )); -} - -# Return the network address of -# an IPv4 address -sub ipv4_network($;$) { - my ($ip,$cidr) = ipv4_parse( $_[0], $_[1] ); - - # If only an host is given, use the default netmask - unless ($cidr) { - $cidr = ipv4_msk2cidr( ipv4_dflt_netmask($ip) ); - } - my $u32 = unpack "N", pack "CCCC", split /\./, $ip; - my $bits = "1" x $cidr . "0" x (32 - $cidr ); - - my $msk = unpack "N", pack "B*", $bits; - - my $net = join ".", unpack "CCCC", pack "N", $u32 & $msk; - - wantarray ? ( $net, $cidr) : "$net/$cidr"; -} - -sub ipv4_broadcast($;$) { - my ($ip,$cidr) = ipv4_parse( $_[0], $_[1] ); - - # If only an host is given, use the default netmask - unless ($cidr) { - $cidr = ipv4_msk2cidr( ipv4_dflt_netmask($ip) ); - } - - my $u32 = unpack "N", pack "CCCC", split /\./, $ip; - my $bits = "1" x $cidr . "0" x (32 - $cidr ); - - my $msk = unpack "N", pack "B*", $bits; - - my $broadcast = join ".", unpack "CCCC", pack "N", $u32 | ~$msk; - - $broadcast; -} - -sub ipv4_in_network($$;$$) { - my ($ip1,$cidr1,$ip2,$cidr2); - if ( @_ >= 3) { - ($ip1,$cidr1) = ipv4_parse( $_[0], $_[1] ); - ($ip2,$cidr2) = ipv4_parse( $_[2], $_[3] ); - } else { - ($ip1,$cidr1) = ipv4_parse( $_[0]); - ($ip2,$cidr2) = ipv4_parse( $_[1]); - } - - # Check for magic addresses. - return 1 if $ip1 eq "255.255.255.255" or $ip1 eq "0.0.0.0"; - return 1 if $ip2 eq "255.255.255.255" or $ip2 eq "0.0.0.0"; - - # Case where first argument is really an host - return $ip1 eq $ip2 unless ($cidr1); - - # Case where second argument is an host - if ( not defined $cidr2) { - return ipv4_network( $ip1, $cidr1) eq ipv4_network( $ip2, $cidr1 ); - } elsif ( $cidr2 > $cidr1 ) { - # Netmask 2 is more specific than netmask 1 - return ipv4_network( $ip1, $cidr2) eq ipv4_network( $ip2, $cidr2); - } else { - # Netmask 1 is more specific than netmask 2 - return ipv4_network( $ip1, $cidr1) eq ipv4_network( $ip2, $cidr1); - } -} -# Autoload methods go after =cut, and are processed by the autosplit program. - -1; -__END__ -# Below is the stub of documentation for your module. You better edit it! -=pod - -=head1 NAME - -Network::IPv4Addr - Perl extension for manipulating IPv4 addresses. - -=head1 SYNOPSIS - - use Network::IPv4Addr qw( :all ); - - my ($ip,$cidr) = ipv4_parse( "127.0.0.1/24" ); - my ($ip,$cidr) = ipv4_parse( "192.168.100.10 / 255.255.255.0" ); - - my ($net,$msk) = ipv4_network( "192.168.100.30" ); - - my $broadcast = ipv4_broadcast( "192.168.100.30/26" ); - - if ( ipv4_in_network( "192.168.100.0", $her_ip ) ) { - print "Welcome !"; - } - - etc. - -=head1 DESCRIPTION - -Network::IPv4Addr provides functions for parsing IPv4 addresses both -in traditional address/netmask format and in the new CIDR format. -There are also methods for calculating the network and broadcast -address and also to see check if a given address is in a specific -network. - -=head1 ADDRESSES - -All of Network::IPv4Addr functions accepts addresses in many -format. The parsing is very liberal. - -All these addresses would be accepted: - - 127.0.0.1 - 192.168.001.010/24 - 192.168.10.10/255.255.255.0 - 192.168.30.10 / 21 - 10.0.0.0 / 255.0.0.0 - 255.255.0.0 - -Those wouldn't though: - - 272.135.234.0 - 192.168/16 - -Most functions accepts the address and netmask or masklength in the -same scalar value or as separate values. That is either - - my($ip,$masklength) = ipv4_parse($cidr_str); - my($ip,$masklength) = ipv4_parse($ip_str,$msk_str); - -=head1 USING - -No functions are exported by default. Either use the C<:all> tag -to import them all or explicitly import those you need. - -=head1 FUNCTIONS - -=over - -=item ipv4_parse - - my ($ip,$msklen) = ipv4_parse($cidr_str); - my $cidr = ipv4_parse($ip_str,$msk_str); - my ($ip) = ipv4_parse($ip_str,$msk_str); - -Parse an IPv4 address and in scalar context the address in CIDR -format and in an array context the address and the mask length. - -If the parameters doesn't contains a netmask or a mask length, -in scalar context only the IPv4 address is returned and in an -array context the mask length is undefined. - -If the function cannot parse its input, it croaks. Trap it using -C if don't like that. - -=item ipv4_network - - my $cidr = ipv4_network($ip_str); - my $cidr = ipv4_network($cidr_str); - my ($net,$msk) = ipv4_network( $net_str, $msk_str); - -In scalar context, this function returns the network in CIDR format in -which the address is. In array context, it returns the network address and -its mask length as a two elements array. If the input is an host without -a netmask of mask length, the default netmask is assumed. - -Again, the function croak if the input is invalid. - -=item ipv4_broadcast - - my ($broadcast) = ipv4_broadcast($ip_str); - my $broadcast = ipv4_broadcast($ip_str,$msk_str); - -This function returns the broadcast address. If the input doesn't -contains a netmask or mask length, the default netmask is assumed. - -This function croaks if the input is invalid. - -=item ipv4_network - - my $cidr = ipv4_network($net_str); - my $cidr = ipv4_network($cidr_sstr); - my ($net,$msk) = ipv4_network( $ip_str, $mask_str); - -In scalar context, this function returns the network in CIDR format in -which the address is. In array context, it returns the network address and -its mask length as a two elements array. If the input is an host without -a netmask or mask length, the default netmask is assumed. - -Again, the function croak if the input is invalid. - -=item ipv4_in_network - - print "Yes" if ipv4_in_network( $cidr_str1, $cidr_str2); - print "Yes" if ipv4_in_network( $ip_str1, $mask_str1, $cidr_str2 ); - print "Yes" if ipv4_in_network( $ip1, $mask1, $ip2, $msk2 ); - -This function checks if the second network is contained in -the first one and it implements the following semantics : - - If net1 or net2 is a magic address (0.0.0.0 or 255.255.255.255) - than this function returns true. - - If net1 is an host, net2 will be in the same net only if - it is the same host. - - If net2 is an host, it will be contained in net1 only if - it is part of net1. - - If net2 is only part of net1 if it is entirely contained in - net1. - -Trap bad input with C or else. - -=item ipv4_checkip - - if ($ip = ipv4_checkip($str) ) { - # Do something - } - -Return the IPv4 address in the string or undef if the input -doesn't contains a valid IPv4 address. - -=item ipv4_cidr2msk - - my $netmask = ipv4_cidr2msk( $cidr ); - -Returns the netmask corresponding to the mask length given in input. -As usual, croaks if it doesn't like your input (in this case a number -between 0 and 32). - -=item ipv4_msk2cidr - - my $masklen = ipv4_msk2cidr( $msk ); - -Returns the mask length of the netmask in input. As usual, croaks if it -doesn't like your input. - -=back - -=head1 AUTHOR - -Francis J. Lacoste francis.lacoste@iNsu.COM - -=head1 COPYRIGHT - -Copyright (c) 1999 Francis J. Lacoste and iNsu Innovations Inc. -All rights reserved. - -This program is free software; you can redistribute it and/or modify -it under the terms as perl itself. - -=head1 SEE ALSO - -perl(1) ipv4calc(1). - -=cut - +# IPv4Addr.pm - Perl module to manipulate IPv4 addresses. +# +# Author: Francis J. Lacoste +# +# Copyright (C) 1999, 2000 iNsu Innovations Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms as perl itself. +# + +package Net::IPv4Addr; + +use strict; +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); + +BEGIN { + require Exporter; + require AutoLoader; + + @ISA = qw(Exporter AutoLoader); + + @EXPORT = qw(); + + %EXPORT_TAGS = ( + all => [qw{ ipv4_parse ipv4_chkip + ipv4_network ipv4_broadcast + ipv4_cidr2msk ipv4_msk2cidr + ipv4_in_network ipv4_dflt_netmask + } ], + ); + + @EXPORT_OK = qw(); + + Exporter::export_ok_tags('all'); + + $VERSION = '0.10'; +} + +# Preloaded methods go here. +use Carp; + +# Functions to manipulate IPV4 address +my $ip_rgx = "\\d+\\.\\d+\\.\\d+\\.\\d+"; + +# Given an IPv4 address in host, ip/netmask or cidr format +# returns a ip / cidr pair. +sub ipv4_parse($;$) { + my ($ip,$msk); + # Called with 2 args, assume first is IP address + if ( defined $_[1] ) { + $ip = $_[0]; + $msk= $_[1]; + } else { + ($ip) = $_[0] =~ /($ip_rgx)/o; + ($msk) = $_[0] =~ m!/(.+)!o; + } + + # Remove white spaces + $ip = ipv4_chkip( $ip ) or + croak __PACKAGE__, ": invalid IPv4 address: ", $ip, "\n"; + $msk =~ s/\s//g if defined $msk; + + # Check Netmask to see if it is a CIDR or Network + if (defined $msk ) { + if ($msk =~ /^\d{1,2}$/) { + # Check cidr + croak __PACKAGE__, ": invalid cidr: ", $msk, "\n" + if $msk < 0 or $msk > 32; + } elsif ($msk =~ /^$ip_rgx$/o ) { + $msk = ipv4_msk2cidr($msk); + } else { + croak __PACKAGE__, ": invalid netmask specification: ", $msk, "\n"; + } + } else { + # Host + return $ip; + } + wantarray ? ($ip,$msk) : "$ip/$msk"; +} + +sub ipv4_dflt_netmask($) { + my ($ip) = ipv4_parse($_[0]); + + my ($b1) = split /\./, $ip; + + return "255.0.0.0" if $b1 <= 127; + return "255.255.0.0" if $b1 <= 191; + return "255.255.255.0"; +} + +# Check form a valid IPv4 address. +sub ipv4_chkip($) { + my ($ip) = $_[0] =~ /($ip_rgx)/o; + + return undef unless $ip; + + # Check that bytes are in range + for (split /\./, $ip ) { + return undef if $_ < 0 or $_ > 255; + } + return $ip; +} + +# Transform a netmask in a CIDR mask length +sub ipv4_msk2cidr($) { + my $msk = ipv4_chkip( $_[0] ) + or croak __PACKAGE__, ": invalid netmask: ", $_[0], "\n"; + + my @bytes = split /\./, $msk; + + my $cidr = 0; + for (@bytes) { + my $bits = unpack( "B*", pack( "C", $_ ) ); + $cidr += $bits =~ tr /1/1/; + } + return $cidr; +} + +# Transform a CIDR mask length in a netmask +sub ipv4_cidr2msk($) { + my $cidr = shift; + croak __PACKAGE__, ": invalid cidr: ", $cidr, "\n" + if $cidr < 0 or $cidr > 32; + + my $bits = "1" x $cidr . "0" x (32 - $cidr); + + return join ".", (unpack 'CCCC', pack("B*", $bits )); +} + +# Return the network address of +# an IPv4 address +sub ipv4_network($;$) { + my ($ip,$cidr) = ipv4_parse( $_[0], $_[1] ); + + # If only an host is given, use the default netmask + unless (defined $cidr) { + $cidr = ipv4_msk2cidr( ipv4_dflt_netmask($ip) ); + } + my $u32 = unpack "N", pack "CCCC", split /\./, $ip; + my $bits = "1" x $cidr . "0" x (32 - $cidr ); + + my $msk = unpack "N", pack "B*", $bits; + + my $net = join ".", unpack "CCCC", pack "N", $u32 & $msk; + + wantarray ? ( $net, $cidr) : "$net/$cidr"; +} + +sub ipv4_broadcast($;$) { + my ($ip,$cidr) = ipv4_parse( $_[0], $_[1] ); + + # If only an host is given, use the default netmask + unless (defined $cidr) { + $cidr = ipv4_msk2cidr( ipv4_dflt_netmask($ip) ); + } + + my $u32 = unpack "N", pack "CCCC", split /\./, $ip; + my $bits = "1" x $cidr . "0" x (32 - $cidr ); + + my $msk = unpack "N", pack "B*", $bits; + + my $broadcast = join ".", unpack "CCCC", pack "N", $u32 | ~$msk; + + $broadcast; +} + +sub ipv4_in_network($$;$$) { + my ($ip1,$cidr1,$ip2,$cidr2); + if ( @_ >= 3) { + ($ip1,$cidr1) = ipv4_parse( $_[0], $_[1] ); + ($ip2,$cidr2) = ipv4_parse( $_[2], $_[3] ); + } else { + ($ip1,$cidr1) = ipv4_parse( $_[0]); + ($ip2,$cidr2) = ipv4_parse( $_[1]); + } + + # Check for magic addresses. + return 1 if ($ip1 eq "255.255.255.255" or $ip1 eq "0.0.0.0") + and !defined $cidr1; + return 1 if ($ip2 eq "255.255.255.255" or $ip2 eq "0.0.0.0") + and !defined $cidr2; + + # Case where first argument is really an host + return $ip1 eq $ip2 unless (defined $cidr1); + + # Case where second argument is an host + if ( not defined $cidr2) { + return ipv4_network( $ip1, $cidr1) eq ipv4_network( $ip2, $cidr1 ); + } elsif ( $cidr2 >= $cidr1 ) { + # Network 2 is smaller or equal than network 1 + return ipv4_network( $ip1, $cidr1 ) eq ipv4_network( $ip2, $cidr1 ); + } else { + # Network 2 is bigger, so can't be wholly contained. + return 0; + } +} +# Autoload methods go after =cut, and are processed by the autosplit program. + +1; +__END__ +# Below is the stub of documentation for your module. You better edit it! +=pod + +=head1 NAME + +Net::IPv4Addr - Perl extension for manipulating IPv4 addresses. + +=head1 SYNOPSIS + + use Net::IPv4Addr qw( :all ); + + my ($ip,$cidr) = ipv4_parse( "127.0.0.1/24" ); + my ($ip,$cidr) = ipv4_parse( "192.168.100.10 / 255.255.255.0" ); + + my ($net,$msk) = ipv4_network( "192.168.100.30" ); + + my $broadcast = ipv4_broadcast( "192.168.100.30/26" ); + + if ( ipv4_in_network( "192.168.100.0", $her_ip ) ) { + print "Welcome !"; + } + + etc. + +=head1 DESCRIPTION + +Net::IPv4Addr provides functions for parsing IPv4 addresses both +in traditional address/netmask format and in the new CIDR format. +There are also methods for calculating the network and broadcast +address and also to see check if a given address is in a specific +network. + +=head1 ADDRESSES + +All of Net::IPv4Addr functions accepts addresses in many +format. The parsing is very liberal. + +All these addresses would be accepted: + + 127.0.0.1 + 192.168.001.010/24 + 192.168.10.10/255.255.255.0 + 192.168.30.10 / 21 + 10.0.0.0 / 255.0.0.0 + 255.255.0.0 + +Those wouldn't though: + + 272.135.234.0 + 192.168/16 + +Most functions accepts the address and netmask or masklength in the +same scalar value or as separate values. That is either + + my($ip,$masklength) = ipv4_parse($cidr_str); + my($ip,$masklength) = ipv4_parse($ip_str,$msk_str); + +=head1 USING + +No functions are exported by default. Either use the C<:all> tag +to import them all or explicitly import those you need. + +=head1 FUNCTIONS + +=over + +=item ipv4_parse + + my ($ip,$msklen) = ipv4_parse($cidr_str); + my $cidr = ipv4_parse($ip_str,$msk_str); + my ($ip) = ipv4_parse($ip_str,$msk_str); + +Parse an IPv4 address and in scalar context the address in CIDR +format and in an array context the address and the mask length. + +If the parameters doesn't contains a netmask or a mask length, +in scalar context only the IPv4 address is returned and in an +array context the mask length is undefined. + +If the function cannot parse its input, it croaks. Trap it using +C if don't like that. + +=item ipv4_network + + my $cidr = ipv4_network($ip_str); + my $cidr = ipv4_network($cidr_str); + my ($net,$msk) = ipv4_network( $net_str, $msk_str); + +In scalar context, this function returns the network in CIDR format in +which the address is. In array context, it returns the network address and +its mask length as a two elements array. If the input is an host without +a netmask of mask length, the default netmask is assumed. + +Again, the function croak if the input is invalid. + +=item ipv4_broadcast + + my ($broadcast) = ipv4_broadcast($ip_str); + my $broadcast = ipv4_broadcast($ip_str,$msk_str); + +This function returns the broadcast address. If the input doesn't +contains a netmask or mask length, the default netmask is assumed. + +This function croaks if the input is invalid. + +=item ipv4_network + + my $cidr = ipv4_network($net_str); + my $cidr = ipv4_network($cidr_sstr); + my ($net,$msk) = ipv4_network( $ip_str, $mask_str); + +In scalar context, this function returns the network in CIDR format in +which the address is. In array context, it returns the network address and +its mask length as a two elements array. If the input is an host without +a netmask or mask length, the default netmask is assumed. + +Again, the function croak if the input is invalid. + +=item ipv4_in_network + + print "Yes" if ipv4_in_network( $cidr_str1, $cidr_str2); + print "Yes" if ipv4_in_network( $ip_str1, $mask_str1, $cidr_str2 ); + print "Yes" if ipv4_in_network( $ip1, $mask1, $ip2, $msk2 ); + +This function checks if the second network is contained in +the first one and it implements the following semantics : + + If net1 or net2 is a magic address (0.0.0.0 or 255.255.255.255) + than this function returns true. + + If net1 is an host, net2 will be in the same net only if + it is the same host. + + If net2 is an host, it will be contained in net1 only if + it is part of net1. + + If net2 is only part of net1 if it is entirely contained in + net1. + +Trap bad input with C or else. + +=item ipv4_checkip + + if ($ip = ipv4_checkip($str) ) { + # Do something + } + +Return the IPv4 address in the string or undef if the input +doesn't contains a valid IPv4 address. + +=item ipv4_cidr2msk + + my $netmask = ipv4_cidr2msk( $cidr ); + +Returns the netmask corresponding to the mask length given in input. +As usual, croaks if it doesn't like your input (in this case a number +between 0 and 32). + +=item ipv4_msk2cidr + + my $masklen = ipv4_msk2cidr( $msk ); + +Returns the mask length of the netmask in input. As usual, croaks if it +doesn't like your input. + +=back + +=head1 AUTHOR + +Francis J. Lacoste + +=head1 COPYRIGHT + +Copyright (c) 1999, 2000 iNsu Innovations Inc. +All rights reserved. + +This program is free software; you can redistribute it and/or modify +it under the terms as perl itself. + +=head1 SEE ALSO + +perl(1) ipv4calc(1). + +=cut + diff --git a/CPAN/Net/Ifconfig/Wrapper.pm b/CPAN/Net/Ifconfig/Wrapper.pm new file mode 100644 index 00000000000..8f6b3b82c66 --- /dev/null +++ b/CPAN/Net/Ifconfig/Wrapper.pm @@ -0,0 +1,1114 @@ +package Net::Ifconfig::Wrapper; + +use strict; +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS @EXPORT_FAIL); + +$VERSION = 0.14; + +#$^W++; + +require Exporter; + +@ISA = qw(Exporter); +# Items to export into caller's namespace by default. Note: do not export +# names by default without a very good reason. Use EXPORT_OK instead. +# Do not simply export all your public functions/methods/constants. +@EXPORT = qw(); + +%EXPORT_TAGS = ('Ifconfig' => [qw(Ifconfig)]); + +foreach (keys(%EXPORT_TAGS)) + { push(@{$EXPORT_TAGS{'all'}}, @{$EXPORT_TAGS{$_}}); }; + +$EXPORT_TAGS{'all'} + and @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); + +my $DEBUG = 0; + +use POSIX; +my ($OsName, $OsVers) = (POSIX::uname())[0,2]; + +my $Win32_FormatMessage = undef; +my %Win32API = (); +my %ToLoad = ('iphlpapi' => {'GetAdaptersInfo' => [['P','P'], 'N'], + #'GetIpAddrTable' => [['P','P','I'], 'N'], + 'AddIPAddress' => [['N','N','N','P','P'], 'N'], + 'DeleteIPAddress' => [['N'], 'N'], + }, + ); + +my $Win32_ERROR_BUFFER_OVERFLOW = undef; +my $Win32_ERROR_INSUFFICIENT_BUFFER = undef; +my $Win32_NO_ERROR = undef; + +my $ETHERNET = 'ff:ff:ff:ff:ff:ff'; + +(($^O eq 'openbsd') && + (`/usr/sbin/arp -a 2>&1` =~ m/(?:\A|\n).+\s+at\s+([a-f\d]{1,2}(?:\:[a-f\d]{1,2}){5})\s+static\s*(?:\n|\Z)/i)) + and $ETHERNET = $1; + +if (($^O eq 'MSWin32') || ($^O eq 'cygwin')) + { + eval 'use Win32::API; + use Win32::WinError; + + Win32::IsWinNT() + or die "Only WinNT (from Win2K) is supported"; + + $Win32_FormatMessage = sub { return Win32::FormatMessage(@_); }; + $Win32_ERROR_BUFFER_OVERFLOW = ERROR_BUFFER_OVERFLOW; + $Win32_ERROR_INSUFFICIENT_BUFFER = ERROR_INSUFFICIENT_BUFFER; + $Win32_NO_ERROR = NO_ERROR; + + foreach my $DLib (keys(%ToLoad)) + { + foreach my $Func (keys(%{$ToLoad{$DLib}})) + { + $Win32API{$DLib}{$Func} = Win32::API->new($DLib, $Func, $ToLoad{$DLib}{$Func}->[0], $ToLoad{$DLib}{$Func}->[1]) + or die "Cannot import function \'$Func\' from \'$DLib\' DLL: $^E"; + }; + }; + '; + + $@ and die $@; + }; + +my $MAXLOGIC = 65535; + +my %Hex2Mask = ('00000000' => '0.0.0.0', '80000000' => '128.0.0.0', + 'c0000000' => '192.0.0.0', 'e0000000' => '224.0.0.0', + 'f0000000' => '240.0.0.0', 'f8000000' => '248.0.0.0', + 'fc000000' => '252.0.0.0', 'fe000000' => '254.0.0.0', + 'ff000000' => '255.0.0.0', 'ff800000' => '255.128.0.0', + 'ffc00000' => '255.192.0.0', 'ffe00000' => '255.224.0.0', + 'fff00000' => '255.240.0.0', 'fff80000' => '255.248.0.0', + 'fffc0000' => '255.252.0.0', 'fffe0000' => '255.254.0.0', + 'ffff0000' => '255.255.0.0', 'ffff8000' => '255.255.128.0', + 'ffffc000' => '255.255.192.0', 'ffffe000' => '255.255.224.0', + 'fffff000' => '255.255.240.0', 'fffff800' => '255.255.248.0', + 'fffffc00' => '255.255.252.0', 'fffffe00' => '255.255.254.0', + 'ffffff00' => '255.255.255.0', 'ffffff80' => '255.255.255.128', + 'ffffffc0' => '255.255.255.192', 'ffffffe0' => '255.255.255.224', + 'fffffff0' => '255.255.255.240', 'fffffff8' => '255.255.255.248', + 'fffffffc' => '255.255.255.252', 'fffffffe' => '255.255.255.254', + 'ffffffff' => '255.255.255.255', + ); + +my $Inet2Logic = undef; +my $Logic2Inet = undef; + +my $Name2Index = undef; + +my %Ifconfig = (); + +my $RunCmd = sub($$) + { + my ($CName, $Iface, $Logic, $Addr, $Mask) = @_; + + my $Cmd = (defined($Ifconfig{$CName}{$^O}{$OsName}{$OsVers}{'ifconfig'}) ? + $Ifconfig{$CName}{$^O}{$OsName}{$OsVers}{'ifconfig'} : + $Ifconfig{$CName}{$^O}{'ifconfig'}).' 2>&1'; + + #print "\n=== RunCmd ===\n\$CName: $CName, \$Iface: $Iface, \$Logic: $Logic, \$Addr: $Addr, \$Mask: $Mask\n"; + + $Cmd =~ s{%Iface%}{$Iface}gsex; + $Cmd =~ s{%Logic%}{$Logic}gsex; + $Cmd =~ s{%Addr%}{$Addr}gsex; + $Cmd =~ s{%Mask%}{$Mask}gsex; + + my $saveLang = $ENV{'LANG'} || ''; + $ENV{'LANG'} = 'C'; + my @Output = `$Cmd`; + $ENV{'LANG'} = $saveLang; + + $@ = "Command '$Cmd', exit code '".(defined($?) ? $? : '!UNDEFINED!')."'".join("\t", @Output); + + $? ? return : return \@Output; + }; + +my $SolarisList = sub($$$$) +{ + $Inet2Logic = undef; + $Logic2Inet = undef; + + my $Output = &{$RunCmd}('list', '', '', '', '') + or return; + + $Inet2Logic = {}; + $Logic2Inet = {}; + + my $Iface = undef; + my $Logic = undef; + my $LogUp = undef; + my $Info = {}; + foreach (@{$Output}) + { + if ( + ($_ =~ m/\A([a-z]+\d+)(?:\:(\d+))?\:\s+flags=[^\<]+\<(?:\w+\,)*(up)?(?:\,\w+)*\>.*\n?\Z/io) + || + ($_ =~ m/\A([a-z]+\d+)(?:\:(\d+))?\:\s+flags=[^\<]+\<(?:\w+(?:\,\w+)*)*\>.*\n?\Z/io) + ) + { + $Iface = $1; + $Logic = defined($2) ? $2 : ''; + $LogUp = 1 && $3; + #$Info->{$Iface}{'status'} = ($Info->{$Iface}{'status'} || $LogUp) ? 1 : 0; + $Info->{$Iface}{'status'} = $Info->{$Iface}{'status'} || $LogUp; + } + elsif (!$Iface) + { + next; + } + elsif ( + ($_ =~ m/\A\s+inet\s+(\d{1,3}(?:\.\d{1,3}){3})\s+netmask\s+(?:0x)?([a-f\d]{8})(?:\s.*)?\n?\Z/io) + || + 0 + ) + { + $LogUp + and $Info->{$Iface}{'inet'}{$1} = $Hex2Mask{$2}; + $Inet2Logic->{$Iface}{$1} = $Logic; + $Logic2Inet->{$Iface}{$Logic} = $1; + } + elsif (($_ =~ m/\A\s+media\:?\s+(ethernet.*)\s*\n?\Z/io) && !$Info->{$Iface}{'ether'}) + { + $Info->{$Iface}{'ether'} = $ETHERNET; + if (!$Info->{$Iface}{'media'}) + {$Info->{$Iface}{'media'} = $1; }; + } + elsif (($_ =~ m/\A\s+supported\s+media\:?\s+(.*)\s*\n?\Z/io) && !$Info->{$Iface}{'media'}) + { + $Info->{$Iface}{'media'} = $1; + } + elsif ($_ =~ m/\A\s+ether\s+([a-f\d]{1,2}(?:\:[a-f\d]{1,2}){5})(?:\s.*)?\n?\Z/io) + { + $Info->{$Iface}{'ether'} = $1; + }; + }; + + return $Info; + }; + +my $LinuxList = sub($$$$) + { + # warn " DDD start sub LinuxList...\n"; + $Inet2Logic = undef; + $Logic2Inet = undef; + + my $Output = &{$RunCmd}('list', '', '', '', '') + or return; + + $Inet2Logic = {}; + $Logic2Inet = {}; + + my $Iface = undef; + my $Logic = undef; + my $Info = {}; + foreach (@{$Output}) + { + $DEBUG && warn " DDD looking at line of Output=$_"; + if ( + ($_ =~ m/\A([a-z0-9]+)(?:\:(\d+))?\s+link\s+encap\:(?:ethernet\s+hwaddr\s+([a-f\d]{1,2}(?:\:[a-f\d]{1,2}){5}))?.*\n?\Z/io) + || + # German locale de_DE.UTF-8 + ($_ =~ m/\A([a-z0-9]+)(?:\:(\d+))?\s+Link\s+encap\:(?:Ethernet\s+Hardware\s+Adresse\s+([a-f\d]{1,2}(?:\:[a-f\d]{1,2}){5}))?.*\n?\Z/io) + ) + { + $Iface = $1; + $Logic = defined($2) ? $2 : ''; + defined($3) + and $Info->{$Iface}{'ether'} = $3; + $Info->{$Iface}{'status'} = 0; + } + elsif ( + ($_ =~ m/\A([a-z0-9]+)\:\s+flags=\d+<(\w+(?:,\w+)*)*>.*\n?\Z/io) + ) + { + $Iface = $1; + my $sFlags = $2; + $DEBUG && warn " DDD matched 'flags' line, Iface=$Iface, sFlags=$sFlags\n"; + $Info->{$Iface}{'status'} = 1 if ($sFlags =~ m/\bUP\b/); + } + elsif (!$Iface) + { + next; + } + elsif ( + ($_ =~ m/\A\s+inet\s+addr\:(\d{1,3}(?:\.\d{1,3}){3})\s+(?:.*\s)?mask\:(\d{1,3}(?:\.\d{1,3}){3}).*\n?\Z/io) + || + ($_ =~ m/\A\s+inet\s+(\d{1,3}(?:\.\d{1,3}){3})\s+netmask\s+(\d{1,3}(?:\.\d{1,3}){3})(?:\s.*)?\n?\Z/io) + || + # German locale de_DE.UTF-8 + ($_ =~ m/\A\s+inet\s+Adresse\:(\d{1,3}(?:\.\d{1,3}){3})\s+(?:.*\s)?Maske\:(\d{1,3}(?:\.\d{1,3}){3}).*\n?\Z/io) + ) + { + my $sIP = $1; + my $sNetmask = $2; + $DEBUG && warn " DDD matched 'netmask' line, sIP=$sIP, sNetmask=$sNetmask\n"; + $Info->{$Iface}{'inet'}{$sIP} = $sNetmask; + $Inet2Logic->{$Iface}{$sIP} = $Logic; + $Logic2Inet->{$Iface}{$Logic} = $sIP; + } + elsif ($_ =~ m/\A\s+ether\s+([a-f0-9]{1,2}(?:\:[a-f0-9]{1,2}){5})(?:\s|\n|\Z)/io) + { + $Info->{$Iface}{'ether'} = $1; + } + elsif ($_ =~ m/\A\s+up(?:\s+[^\s]+)*\s*\n?\Z/io) + { + $DEBUG && warn " DDD matched 'up' line\n"; + $Info->{$Iface}{'status'} = 1; + }; + }; + + return $Info; + }; + + +my $st_IP_ADDR_STRING = + ['Next' => 'L', #struct _IP_ADDR_STRING* + 'IpAddress' => 'a16', #IP_ADDRESS_STRING + 'IpMask' => 'a16', #IP_MASK_STRING + 'Context' => 'L' #DWORD + ]; + +my $MAX_ADAPTER_NAME_LENGTH = 256; +my $MAX_ADAPTER_DESCRIPTION_LENGTH = 128; +my $MAX_ADAPTER_ADDRESS_LENGTH = 8; + +my $st_IP_ADAPTER_INFO = + ['Next' => 'L', #struct _IP_ADAPTER_INFO* + 'ComboIndex' => 'L', #DWORD + 'AdapterName' => 'a'.($MAX_ADAPTER_NAME_LENGTH+4), #char[MAX_ADAPTER_NAME_LENGTH + 4] + 'Description' => 'a'.($MAX_ADAPTER_DESCRIPTION_LENGTH+4), #char[MAX_ADAPTER_DESCRIPTION_LENGTH + 4] + 'AddressLength' => 'L', #UINT + 'Address' => 'a'.$MAX_ADAPTER_ADDRESS_LENGTH, #BYTE[MAX_ADAPTER_ADDRESS_LENGTH] + 'Index' => 'L', #DWORD + 'Type' => 'L', #UINT + 'DhcpEnabled' => 'L', #UINT + 'CurrentIpAddress' => 'L', #PIP_ADDR_STRING + 'IpAddressList' => $st_IP_ADDR_STRING, #IP_ADDR_STRING + 'GatewayList' => $st_IP_ADDR_STRING, #IP_ADDR_STRING + 'DhcpServer' => $st_IP_ADDR_STRING, #IP_ADDR_STRING + 'HaveWins' => 'L', #BOOL + 'PrimaryWinsServer' => $st_IP_ADDR_STRING, #IP_ADDR_STRING + 'SecondaryWinsServer' => $st_IP_ADDR_STRING, #IP_ADDR_STRING + 'LeaseObtained' => 'L', #time_t + 'LeaseExpires' => 'L', #time_t + ]; + +#my $st_MIB_IPADDRROW = +# ['dwAddr' => 'L', #DWORD +# 'dwIndex' => 'L', #DWORD +# 'dwMask' => 'L', #DWORD +# 'dwBCastAddr' => 'L', #DWORD +# 'dwReasmSize' => 'L', #DWORD +# 'unused1' => 'S', #unsigned short +# 'unused2' => 'S', #unsigned short +# ]; + +my %UnpackStrCache = (); +my $UnpackStr = undef; +$UnpackStr = sub($$) + { + my ($Struct, $Repeat) = @_; + $Repeat or $Repeat = 1; + + my $StructUpStr = ''; + + if (!defined($UnpackStrCache{$Struct})) + { + for (my $RI = 1; defined($Struct->[$RI]); $RI += 2) + { + $StructUpStr .= ref($Struct->[$RI]) ? + &{$UnpackStr}($Struct->[$RI], 1) : + $Struct->[$RI]; + }; + $UnpackStrCache{$Struct} = $StructUpStr; + } + else + { $StructUpStr = $UnpackStrCache{$Struct}; }; + + my $UpStr = ''; + for (; $Repeat > 0; $Repeat--) + { $UpStr .= $StructUpStr; }; + + return $UpStr; + }; + + +my $ShiftStruct = undef; +$ShiftStruct = sub($$) + { + my ($Array, $Struct) = @_; + + my $Result = {}; + #tie(%{$Result}, 'Tie::IxHash'); + + for (my $RI = 0; defined($Struct->[$RI]); $RI += 2) + { + $Result->{$Struct->[$RI]} = ref($Struct->[$RI+1]) ? + &{$ShiftStruct}($Array, $Struct->[$RI+1]) : + shift(@{$Array}); + }; + return $Result; + }; + +my $UnpackStruct = sub($$) + { + my ($pBuff, $Struct) = @_; + + my $UpStr = &{$UnpackStr}($Struct); + + my @Array = unpack($UpStr, ${$pBuff}); + + substr(${$pBuff}, 0, length(pack($UpStr)), ''); + + return &{$ShiftStruct}(\@Array, $Struct); + }; + + +my $if_hwaddr = sub($$) + { + my($len, $addr) = @_; + return join(':', map {sprintf '%02x', $_ } unpack('C' x $len, $addr)); + }; + +sub if_ipaddr + { + my ($addr) = @_; + return join(".", unpack("C4", pack("L", $addr))); + }; + +my $Win32List = sub($$$$) + { + $Inet2Logic = undef; + $Logic2Inet = undef; + $Name2Index = undef; + + my $Buff = ''; + my $BuffLen = pack('L', 0); + + my $Res = $Win32API{'iphlpapi'}{'GetAdaptersInfo'}->Call(0, $BuffLen); + + while ($Res == $Win32_ERROR_BUFFER_OVERFLOW) + { + $Buff = "\0" x unpack("L", $BuffLen); + $Res = $Win32API{'iphlpapi'}{'GetAdaptersInfo'}->Call($Buff, $BuffLen); + }; + + if ($Res != $Win32_NO_ERROR) + { + $! = $Res; + $@ = "Error running 'GetAdaptersInfo' function: ".&{$Win32_FormatMessage}($Res); + return; + }; + + my $Info = {}; + + $Inet2Logic = {}; + $Logic2Inet = {}; + $Name2Index = {}; + + while (1) + { + my $ADAPTER_INFO = &{$UnpackStruct}(\$Buff, $st_IP_ADAPTER_INFO); + + foreach my $Field ('AdapterName', 'Description') + { $ADAPTER_INFO->{$Field} =~ s/\x00+\Z//o; }; + + foreach my $AddrField ('IpAddressList', 'GatewayList', 'DhcpServer', 'PrimaryWinsServer', 'SecondaryWinsServer') + { + foreach my $Field ('IpAddress', 'IpMask') + { $ADAPTER_INFO->{$AddrField}{$Field} =~ s/\x00+\Z//o; }; + }; + + + $ADAPTER_INFO->{'Address'} = &{$if_hwaddr}($ADAPTER_INFO->{'AddressLength'}, $ADAPTER_INFO->{'Address'}); + + foreach my $IpList ('IpAddressList', 'GatewayList') + { + my $ADDR_STRING = $ADAPTER_INFO->{$IpList}; + $ADAPTER_INFO->{$IpList} = [$ADDR_STRING,]; + while ($ADDR_STRING->{'Next'}) + { + $ADDR_STRING = &{$UnpackStruct}(\$Buff, $st_IP_ADDR_STRING); + foreach my $Field ('IpAddress', 'IpMask') + { $ADDR_STRING->{$Field} =~ s/\x00+\Z//o; }; + push(@{$ADAPTER_INFO->{$IpList}}, $ADDR_STRING); + }; + }; + + my $Iface = $ADAPTER_INFO->{'AdapterName'}; + + $Info->{$Iface}{'descr'} = $ADAPTER_INFO->{'Description'}; + $Info->{$Iface}{'ether'} = $ADAPTER_INFO->{'Address'}; + $Info->{$Iface}{'status'} = 1; + + foreach my $Addr (@{$ADAPTER_INFO->{'IpAddressList'}}) + { + ($Addr->{'IpAddress'} eq '0.0.0.0') + and next; + $Info->{$Iface}{'inet'}{$Addr->{'IpAddress'}} = $Addr->{'IpMask'}; + $Inet2Logic->{$Iface}{$Addr->{'IpAddress'}} = $Addr->{'Context'}; + $Logic2Inet->{$Iface}{$Addr->{'Context'}} = $Addr->{'IpAddress'}; + }; + + $Name2Index->{$Iface} = $ADAPTER_INFO->{'Index'}; + + $ADAPTER_INFO->{'Next'} + or last; + }; + + + #$Buff = ''; + #$BuffLen = pack('L', 0); + #$Res = $Win32API{'iphlpapi'}{'GetIpAddrTable'}->Call($Buff, $BuffLen, 0); + # + #while ($Res == ERROR_INSUFFICIENT_BUFFER) + # { + # $Buff = "\0" x unpack("L", $BuffLen); + # $Res = $Win32API{'iphlpapi'}{'GetIpAddrTable'}->Call($Buff, $BuffLen, 0); + # }; + # + #if ($Res != $Win32_NO_ERROR) + # { + # $! = $Res; + # $@ = "Error running 'GetIpAddrTable' function: ".&{$Win32_FormatMessage}($Res); + # return; + # }; + # + #my $IpAddrTable = &{$UnpackStruct}(\$Buff, ['Len' => 'L']); + #my %Info1 = (); + #for (; $IpAddrTable->{'Len'} > 0; $IpAddrTable->{'Len'}--) + # { + # my $IPADDRROW = &{$UnpackStruct}(\$Buff, $st_MIB_IPADDRROW); + # $Info->{$IPADDRROW->{'dwIndex'}} + # and next; + # $Info1{$IPADDRROW->{'dwIndex'}}{'inet'}{if_ipaddr($IPADDRROW->{'dwAddr'})} = if_ipaddr($IPADDRROW->{'dwMask'}); + # }; + # + #foreach my $Iface (keys(%Info1)) + # { $Info->{$Iface} = $Info1{$Iface}; }; + + return wantarray ? %{$Info} : $Info; + }; + + + +$Ifconfig{'list'} = {'solaris' => {'ifconfig' => 'LC_ALL=C /sbin/ifconfig -a', + 'function' => $SolarisList}, + 'openbsd' => {'ifconfig' => 'LC_ALL=C /sbin/ifconfig -A', + 'function' => $SolarisList}, + 'linux' => {'ifconfig' => 'LC_ALL=C /sbin/ifconfig -a', + 'function' => $LinuxList}, + 'MSWin32' => {'ifconfig' => '', + 'function' => $Win32List,}, + }; + +$Ifconfig{'list'}{'freebsd'} = $Ifconfig{'list'}{'solaris'}; +$Ifconfig{'list'}{'darwin'} = $Ifconfig{'list'}{'solaris'}; +$Ifconfig{'list'}{'cygwin'} = $Ifconfig{'list'}{'MSWin32'}; + + +my $UpDown = sub($$$$) + { + my ($CName, $Iface, $Addr, $Mask) = @_; + + if (!(defined($Iface) && defined($Addr) && defined($Mask))) + { + $@ = "Command '$CName': interface, inet address and netmask have to be defined"; + return; + }; + + my $Output = &{$RunCmd}($CName, $Iface, '', $Addr, $Mask); + + $Inet2Logic = undef; + $Logic2Inet = undef; + + $Output ? return $Output : return; + }; + +my $UpDownNewLog = sub($$$$) + { + my ($CName, $Iface, $Addr, $Mask) = @_; + + if (!(defined($Iface) && defined($Addr) && defined($Mask))) + { + $@ = "Command '$CName': interface, inet address and netmask have to be defined"; + return; + }; + + defined($Inet2Logic) + or (defined($Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}) ? + &{$Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}}() : + &{$Ifconfig{'list'}{$^O}{'function'}}()) + or return; + + my $Logic = $Inet2Logic->{$Iface}{$Addr}; + + my $RunIndex = 1; + for(; !defined($Logic); $RunIndex++) + { + if ($RunIndex > $MAXLOGIC) + { + $@ = "Command '$CName': maximum number of logic interfaces ($MAXLOGIC) on interface '$Iface' exceeded"; + return; + }; + defined($Logic2Inet->{$Iface}{$RunIndex}) + or $Logic = $RunIndex; + }; + + my $Output = &{$RunCmd}($CName, $Iface, $Logic, $Addr, $Mask); + + $Inet2Logic = undef; + $Logic2Inet = undef; + + $Output ? return $Output : return; + }; + +my $UpDownReqLog = sub($$$$) + { + my ($CName, $Iface, $Addr, $Mask) = @_; + + if (!(defined($Iface) && defined($Addr) && defined($Mask))) + { + $@ = "Command '$CName': interface, inet address and netmask have to be defined"; + return; + }; + + defined($Inet2Logic) + or (defined($Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}) ? + &{$Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}}() : + &{$Ifconfig{'list'}{$^O}{'function'}}()) + or return; + + my $Logic = $Inet2Logic->{$Iface}{$Addr}; + + if (!defined($Logic)) + { + $@ = "Command '$CName': can not get logic interface for interface '$Iface', inet address '$Addr'"; + return; + }; + + my $Output = &{$RunCmd}($CName, $Iface, $Logic, $Addr, $Mask); + + $Inet2Logic = undef; + $Logic2Inet = undef; + + $Output ? return $Output : return; + }; + +#my $Win32UpDown = sub($$) +# { +# my ($Iface, $State) = @_; +# +# +# }; +# +#my $Win32Inet = sub($$$$) +# { +# my ($CName, $Iface, $Addr, $Mask) = @_; +# +# +# if (!(defined($Iface) && defined($Addr) && defined($Mask))) +# { +# $@ = "Command '$CName': interface, inet address and netmask have to be defined"; +# return; +# }; +# +# $Win32Up($Iface) +# or return; +# +# $Win32AddIP($Iface, $Addr, $Mask) +# or return; +# my $Output = &{$RunCmd}('inet', '$Iface', '', '$Addr', '$Mask'); +# +# $Inet2Logic = undef; +# $Logic2Inet = undef; +# +# $Output ? return $Output : return; +# }; + + +my $PackIP = sub($) + { + my @Bytes = split('\.', $_[0]); + return unpack("L", pack('C4', @Bytes)); + }; + +my $Win32AddAlias = sub($$$$) + { + my ($CName, $Iface, $Addr, $Mask) = @_; + + if (!(defined($Iface) && defined($Addr) && defined($Mask))) + { + $@ = "Command '$CName': interface, inet address and netmask have to be defined"; + return; + }; + + defined($Inet2Logic) + or (defined($Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}) ? + &{$Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}}() : + &{$Ifconfig{'list'}{$^O}{'function'}}()) + or return; + + my $NTEContext = pack('L', 0); + my $NTEInstance = pack('L', 0); + + my $Index = $Name2Index->{$Iface}; + + if (!defined($Index)) + { + $@ = "Command '$CName': can not get interface index for interface '$Iface'"; + return; + }; + + my $Res = $Win32API{'iphlpapi'}{'AddIPAddress'}->Call(&{$PackIP}($Addr), &{$PackIP}($Mask), $Index, $NTEContext, $NTEInstance); + + if ($Res != $Win32_NO_ERROR) + { + $! = $Res; + $@ = &{$Win32_FormatMessage}($Res) + or $@ = 'Unknown error :('; + return; + }; + + $Inet2Logic = undef; + $Logic2Inet = undef; + + return ['Command completed successfully']; + }; + +my $Win32RemAlias = sub($$$$) + { + my ($CName, $Iface, $Addr, $Mask) = @_; + + if (!(defined($Iface) && defined($Addr) && defined($Mask))) + { + $@ = "Command '$CName': interface, inet address and netmask have to be defined"; + return; + }; + + defined($Inet2Logic) + or (defined($Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}) ? + &{$Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}}() : + &{$Ifconfig{'list'}{$^O}{'function'}}()) + or return; + + my $Logic = $Inet2Logic->{$Iface}{$Addr}; + + if (!defined($Logic)) + { + $@ = "Command '$CName': can not get logic interface for interface '$Iface', inet address '$Addr'"; + return; + }; + + my $Res = $Win32API{'iphlpapi'}{'DeleteIPAddress'}->Call($Logic); + + if ($Res != $Win32_NO_ERROR) + { + $! = $Res; + $@ = &{$Win32_FormatMessage}($Res); + return; + }; + + $Inet2Logic = undef; + $Logic2Inet = undef; + + return ['Command completed successfully']; + }; + + +$Ifconfig{'inet'} = {'solaris' => {'ifconfig' => '/sbin/ifconfig %Iface% inet %Addr% netmask %Mask% up', + 'function' => $UpDown}, +# 'MSWin32' => {'ifconfig' => '', +# 'function' => $Win32Inet,}, + }; +$Ifconfig{'inet'}{'freebsd'} = $Ifconfig{'inet'}{'solaris'}; +$Ifconfig{'inet'}{'openbsd'} = $Ifconfig{'inet'}{'solaris'}; +$Ifconfig{'inet'}{'linux'} = $Ifconfig{'inet'}{'solaris'}; +$Ifconfig{'inet'}{'darwin'} = $Ifconfig{'inet'}{'solaris'}; + +$Ifconfig{'up'} = $Ifconfig{'inet'}; + +$Ifconfig{'down'}{'solaris'} = {'ifconfig' => '/sbin/ifconfig %Iface% down', + 'function' => $UpDown, + }; +$Ifconfig{'down'}{'freebsd'} = $Ifconfig{'down'}{'solaris'}; +$Ifconfig{'down'}{'openbsd'} = $Ifconfig{'down'}{'solaris'}; +$Ifconfig{'down'}{'linux'} = $Ifconfig{'down'}{'solaris'}; +$Ifconfig{'down'}{'darwin'} = $Ifconfig{'down'}{'solaris'}; + +$Ifconfig{'+alias'} = {'freebsd' => {'ifconfig' => '/sbin/ifconfig %Iface% inet %Addr% netmask %Mask% alias', + 'function' => $UpDown}, + 'solaris' => {'ifconfig' => '/sbin/ifconfig %Iface%:%Logic% inet %Addr% netmask %Mask% up', + 'function' => $UpDownNewLog}, + 'MSWin32' => {'ifconfig' => '', + 'function' => $Win32AddAlias,}, + }; +$Ifconfig{'+alias'}{'openbsd'} = $Ifconfig{'+alias'}{'freebsd'}; +$Ifconfig{'+alias'}{'linux'} = $Ifconfig{'+alias'}{'solaris'}; +$Ifconfig{'+alias'}{'darwin'} = $Ifconfig{'+alias'}{'freebsd'}; + +$Ifconfig{'+alias'}{'solaris'}{'SunOS'}{'5.8'}{'ifconfig'} = '/sbin/ifconfig %Iface%:%Logic% plumb; /sbin/ifconfig %Iface%:%Logic% inet %Addr% netmask %Mask% up'; +$Ifconfig{'+alias'}{'solaris'}{'SunOS'}{'5.9'}{'ifconfig'} = $Ifconfig{'+alias'}{'solaris'}{'SunOS'}{'5.8'}{'ifconfig'}; +$Ifconfig{'+alias'}{'solaris'}{'SunOS'}{'5.10'}{'ifconfig'} = $Ifconfig{'+alias'}{'solaris'}{'SunOS'}{'5.8'}{'ifconfig'}; + +$Ifconfig{'alias'} = $Ifconfig{'+alias'}; + + +$Ifconfig{'-alias'} = {'freebsd' => {'ifconfig' => '/sbin/ifconfig %Iface% inet %Addr% -alias', + 'function' => $UpDown}, + 'solaris' => {'ifconfig' => '/sbin/ifconfig %Iface%:%Logic% down', + 'function' => $UpDownReqLog}, + 'MSWin32' => {'ifconfig' => '', + 'function' => $Win32RemAlias,}, + }; +$Ifconfig{'-alias'}{'openbsd'} = $Ifconfig{'-alias'}{'freebsd'}; +$Ifconfig{'-alias'}{'linux'} = $Ifconfig{'-alias'}{'solaris'}; +$Ifconfig{'-alias'}{'darwin'} = $Ifconfig{'-alias'}{'freebsd'}; + +$Ifconfig{'-alias'}{'solaris'}{'SunOS'}{'5.9'}{'ifconfig'} = '/sbin/ifconfig %Iface%:%Logic% unplumb'; + +sub Ifconfig + { + my ($CName, $Iface, $Addr, $Mask) = @_; + if (!($CName && $Ifconfig{$CName} && $Ifconfig{$CName}{$^O})) + { + $@ = "Command '$CName' is not defined for system '$^O'"; + return; + }; + + defined($Inet2Logic) + or (defined($Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}) ? + &{$Ifconfig{'list'}{$^O}{$OsName}{$OsVers}{'function'}}() : + &{$Ifconfig{'list'}{$^O}{'function'}}()) + or return; + + my $Output = (defined($Ifconfig{$CName}{$^O}{$OsName}{$OsVers}{'function'}) ? + &{$Ifconfig{$CName}{$^O}{$OsName}{$OsVers}{'function'}}($CName, $Iface, $Addr, $Mask) : + &{$Ifconfig{$CName}{$^O}{'function'}}($CName, $Iface, $Addr, $Mask)); + + $Output ? return $Output : return; + }; + +1; + +__END__ + +=head1 NAME + +Net::Ifconfig::Wrapper - provides a unified way to configure network interfaces +on FreeBSD, OpenBSD, Solaris, Linux, OS X, and WinNT (from Win2K). + +=head1 SYNOPSIS + + use Net::Ifconfig::Wrapper; + my $rhInfo = Net::Ifconfig::Wrapper::Ifconfig('list'); + +=head1 DESCRIPTION + +This module provides a unified way to configure the network interfaces +on FreeBSD, OpenBSD, Solaris, Linux, OS X, and WinNT (from Win2K) systems. + +I (IPv4) and C (MAC) addresses are supported at this time>> + +On Unix, this module calls the system C command to gather the information. +On Windows, the functions from IpHlpAPI.DLL are called. + +For all supported Unixes, C expects the C +command to be C. + +See the top-level README file for a list of tested OSes. + +I + +=head1 The Net::Ifconfig::Wrapper methods + +=over 4 + +=item C, I, I
, I);> + +The one and only method of the C module. Does all the jobs. +The particular action is described by the C<$Command> parameter. + +C<$Command> could be: + +=over 8 + +=item 'list' + +C will return a reference to a hash +containing information about interfaces. + +The structure of this hash is the following: + + {IfaceName => {'status' => 0|1 # The status of the interface. 0 means down, 1 means up + 'ether' => MACaddr, # The ethernet address of the interface if available + 'descr' => Description, # The description of the interface if available + 'inet' => {IPaddr1 => NetMask, # The IP address and his netmask, both are in AAA.BBB.CCC.DDD notation + IPaddr2 => NetMask, + ... + }, + ... + }; + + +I, I
, I parameters are ignored. + +The following shows what program is called for each OS: + +=over 12 + +=item FreeBSD + +C + +=item Solaris + +C + +=item OpenBSD + +C + +=item Linux + +C + +=item OS X + +C + +=item MSWin32 + +C function from C + +=back + +Known Limitations: + +OpenBSD: C command is not returning information about MAC addresses +so we are trying to get it from C<'/usr/sbin/arp -a'> command (first I> entry). +If no one present the I> address is returned. + +MSWin32: C function is not returning information about the interface +which has address C<127.0.0.1> bound to it, +so we have no way to return it. + +Not a limitation, but a small problem: in MSWin32, interface names are not human-readable, +they look like C<{843C2077-30EC-4C56-A401-658BB1E42BC7}> (on Win2K at least). + +=item 'inet' + +This function is used to set IPv4 address on interface. It is called as + + Ifconfig('inet', $IfaceName, $Addr, $Mask); + +I> is an interface name as displayed by C<'list'> command + +I> is an IPv4 address in the C notation + +I> is an IPv4 subnet mask in the C notation + +In order to accomplish this, the following actual C programs are called: + +=over 12 + +=item FreeBSD + +C + +=item Solaris + +C + +=item OpenBSD + +C + +=item Linux + +C + +=item OS X + +C + +=item MSWin32: + +nothing :( + +=back + +Known Limitations: + +MSWin32: I did not find a reliable way to recognize the "main" address on the Win32 +network interface, so I have disabled this functionality. If you know how, please let me know. + +=item 'up' + +Just a synonym for C<'inet'> + +=item 'down' + +This function is used to bring specified interface down. It is called as + + Ifconfig('inet', $IfaceName, '', ''); + +I> is an interface name as displayed by C<'list'> command + +Last two arguments are ignored. + +In order to accomplish this, the following programs are called: + +=over 12 + +=item FreeBSD + +C + +=item Solaris + +C + +=item OpenBSD + +C + +=item Linux + +C + +=item OS X + +C + +=item MSWin32 + +Sorry, this function is not possible. + +=back + +Known Limitations: + +MSWin32: I did not find the way to implement the C<'up'> command so I did not implement C<'down'>. + +=item '+alias' + +This function is used to set IPv4 alias address on interface. It have to be called as + + Ifconfig('+alias', $IfaceName, $Addr, $Mask); + +I> is an interface name as displayed by C<'list'> command + +I> is an IPv4 address in the C notation + +I> is an IPv4 subnet mask in the C notation + +In order to accomplish this, the following C programs are called: + +=over 12 + +=item FreeBSD + +C + +=item Solaris + +C + +=item OpenBSD + +C + +=item Linux + +C + +=item OS X + +C + +=item MSWin32 + +C function from C + +=back + +I + +=item 'alias' + +Just a synonim for C<'+alias'> + +=item '-alias' + +This function is used to remove IPv4 alias address from interface. It have to be called as + + Ifconfig('-alias', $IfaceName, $Addr, ''); + +I> is an interface name as displayed by C<'list'> command. + +I> is an IPv4 address in the C notation. + +Last argument is ignored if present. + +In order to accomplish this, the following C programs are called: + +=over 12 + +=item FreeBSD + +C + +=item Solaris + +C + +=item OpenBSD + +C + +=item Linux + +C + +=item OS X + +C + +=item MSWin32 + +C function from C + +=back + +I + +=back + +On success, the C function returns the defined value. +Actually, it is a reference to the array containing the output +of the actual I> program called. + +In case of error, C returns C<'undef'> value, +and the C<$@> variable contains the error message. + +=back + +=head2 EXPORT + +None by default. + +=head1 AUTHOR + +Daniel Podolsky, Etpaba@cpan.orgE +As of 2015-11, maintained by Martin Thurn Emthurn@cpan.orgE + +=head1 SEE ALSO + +L(8), I in I. + +=cut diff --git a/CPAN/PAR.pm b/CPAN/PAR.pm index 31054d10db3..9f9a3af05d7 100644 --- a/CPAN/PAR.pm +++ b/CPAN/PAR.pm @@ -1,5 +1,5 @@ package PAR; -$PAR::VERSION = '0.970'; +$PAR::VERSION = '1.015'; use 5.006; use strict; @@ -7,19 +7,38 @@ use warnings; use Config '%Config'; use Carp qw/croak/; -=head1 NAME +# If the 'prefork' module is available, we +# register various run-time loaded modules with it. +# That way, there is more shared memory in a forking +# environment. +BEGIN { + if (eval 'require prefork') { + prefork->import($_) for qw/ + Archive::Zip + File::Glob + File::Spec + File::Temp + Fcntl + LWP::Simple + PAR::Heavy + /; + # not including Archive::Unzip::Burst which only makes sense + # in the context of a PAR::Packer'ed executable anyway. + } +} -PAR - Perl Archive Toolkit +use PAR::SetupProgname; +use PAR::SetupTemp; -=head1 VERSION +=head1 NAME -This document describes version 0.970 of PAR, released December 3, 2006. +PAR - Perl Archive Toolkit =head1 SYNOPSIS (If you want to make an executable that contains all module, scripts and data files, please consult the L utility instead. L used to be -part of the PAR distribution but is not shipped as part of the L +part of the PAR distribution but is now shipped as part of the L distribution instead.) Following examples assume a F file in Zip format. @@ -98,7 +117,7 @@ F + + + +
[% logLines %]
+ + + \ No newline at end of file diff --git a/HTML/EN/settings/player/alarm.html b/HTML/EN/settings/player/alarm.html index f0d9b558200..33234374026 100644 --- a/HTML/EN/settings/player/alarm.html +++ b/HTML/EN/settings/player/alarm.html @@ -150,7 +150,7 @@ [% END %] [% WRAPPER setting title="SETUP_ALARM_TIMEOUT" desc="SETUP_ALARM_TIMEOUT_DESC" %] - + [% END %] [% WRAPPER setting title="ALARM_FADE" desc="" %] diff --git a/HTML/EN/settings/player/audio.html b/HTML/EN/settings/player/audio.html index 442d9350623..a36cbace985 100644 --- a/HTML/EN/settings/player/audio.html +++ b/HTML/EN/settings/player/audio.html @@ -13,7 +13,7 @@ } %] [%- END -%] - + [% END %] @@ -22,7 +22,7 @@ [% IF prefs.exists('pref_analogOutMode') %] [% WRAPPER settingGroup title="SETUP_ANALOGOUTMODE" desc="SETUP_ANALOGOUTMODE_DESC" %] [% END %] [% END %] - + [% IF prefs.exists('pref_lineInLevel') %] [% WRAPPER settingGroup title="LINE_IN_LEVEL" desc="LINE_IN_LEVEL_DESC" %] [% END %] [% END %] - + [% IF prefs.exists('pref_lineInAlwaysOn') %] [% WRAPPER settingGroup title="LINE_IN_ALWAYS_ON" desc="LINE_IN_ALWAYS_ON_DESC" %] - + [% FOREACH option = { '1' => 'POWEROFFDAC_WHENOFF', '0' => 'POWEROFFDAC_ALWAYSON' } %] [%- END -%] - + [% END %] [% END %] - + [% IF prefs.exists('pref_disableDac') %] [% WRAPPER settingGroup title="SETUP_DISABLEDAC" desc="SETUP_DISABLEDAC_DESC" %] + + [% END %] [% END %] @@ -96,7 +96,7 @@ [% WRAPPER settingSection %] [% WRAPPER settingGroup title="SETUP_TRANSITIONTYPE" desc="SETUP_TRANSITIONTYPE_DESC" %] [% END %] - + [% WRAPPER settingGroup title="SETUP_TRANSITIONSMART" desc="SETUP_TRANSITIONSMART_DESC" %] [% END %] @@ -127,19 +127,23 @@ [% END %] - + [% WRAPPER settingGroup title="SETUP_TRANSITIONDURATION" desc="SETUP_TRANSITIONDURATION_DESC" %] [% END %] + [% WRAPPER settingGroup title="SETUP_FADEINDURATION" desc="SETUP_FADEINDURATION_DESC" %] + + [% END %] + [% END %] [% END %] @@ -147,16 +151,16 @@ [% WRAPPER settingSection %] [% IF prefs.exists('pref_bass') %] [% WRAPPER settingGroup title="BASS" desc="" %] - + [% END %] [% END %] - + [% IF prefs.exists('pref_treble') %] [% WRAPPER settingGroup title="TREBLE" desc="" %] - + [% END %] [% END %] - + [% IF prefs.exists('pref_stereoxl') %] [% WRAPPER settingGroup title="STEREOXL" desc="" %] - + [% FOREACH option = { '1' => 'SETUP_DIGITALVOLUMECONTROL_ON', '0' => 'SETUP_DIGITALVOLUMECONTROL_OFF' } %] [%- END -%] - + [% END %] - - + + [% IF prefs.exists('pref_preampVolumeControl') %] [% WRAPPER settingGroup title="SETUP_PREAMPVOLUMECONTROL" desc="SETUP_PREAMPVOLUMECONTROL_DESC" %] - + [% END %] [% END %] - + [% WRAPPER settingGroup title="SETUP_MP3SILENCEPRELUDE" desc="SETUP_MP3SILENCEPRELUDE_DESC" %] [% END %] - - + + [% IF prefs.exists('pref_digitalOutputEncoding') %] - + [% WRAPPER settingGroup title="SETUP_DIGITALOUTPUTENCODING" desc="SETUP_DIGITALOUTPUTENCODING_DESC" %] [% END %] - + [% END %] - + [% IF prefs.exists('pref_clockSource') %] - + [% WRAPPER settingGroup title="SETUP_CLOCKSOURCE" desc="SETUP_CLOCKSOURCE_DESC" %] [% END %] - + [% END %] [% IF prefs.exists('pref_fxloopSource') %] - + [% WRAPPER settingGroup title="SETUP_FXLOOPSOURCE" desc="SETUP_FXLOOPSOURCE_DESC" %] [% END %] - + [% END %] [% IF prefs.exists('pref_fxloopClock') %] - + [% WRAPPER settingGroup title="SETUP_FXLOOPCLOCK" desc="SETUP_FXLOOPCLOCK_DESC" %] [% END %] - + [% END %] [% IF prefs.exists('pref_polarityInversion') %] - + [% WRAPPER settingGroup title="SETUP_POLARITYINVERSION" desc="SETUP_POLARITYINVERSION_DESC" %] - [% END %] + [% END %] [% END %] [% END %] @@ -300,29 +304,29 @@ [% IF prefs.exists('pref_wordClockOutput') %] [% WRAPPER setting title="SETUP_WORDCLOCKOUTPUT" desc="SETUP_WORDCLOCKOUTPUT_DESC" %] [% END %] [% END %] - + [% IF prefs.exists('pref_rolloffSlow') %] [% WRAPPER setting title="SETUP_ROLLOFFSLOW" desc="SETUP_ROLLOFFSLOW_DESC" %] [% END %] [% END %] @@ -336,14 +340,14 @@ [% WRAPPER settingGroup title="SETUP_MAXBITRATE" %]
[% IF lamefound %] @@ -355,14 +359,14 @@ [% WRAPPER settingGroup title="SETUP_LAMEQUALITY" desc="SETUP_LAMEQUALITY_DESC" %] [% END %] @@ -372,7 +376,7 @@ [% WRAPPER settingSection %] [% WRAPPER settingGroup title="SETUP_REPLAYGAINMODE" desc="SETUP_REPLAYGAINMODE_DESC" %] [% END %] - + [% WRAPPER settingGroup title="SETUP_REPLAYGAIN_REMOTE" desc="SETUP_REPLAYGAIN_REMOTE_DESC" %] [% END %] [% END %] [% END %] - + [% IF prefs.exists('pref_mp3StreamingMethod') %] [% WRAPPER setting title="SETUP_MP3STREAMINGMETHOD" desc="SETUP_MP3STREAMINGMETHOD_DESC" %] + + [% END %] [% END %] - + [% IF prefs.exists('pref_outputChannels') %] [% WRAPPER setting title="SETUP_OUTPUT_CHANNELS" desc="SETUP_OUTPUT_CHANNELS_DESC" %] + + [% END %] - [% END %] + [% END %] [% PROCESS settings/footer.html %] diff --git a/HTML/EN/settings/server/performance.html b/HTML/EN/settings/server/performance.html index 3c10822c50a..d5921206ef9 100644 --- a/HTML/EN/settings/server/performance.html +++ b/HTML/EN/settings/server/performance.html @@ -1,5 +1,5 @@ [% PROCESS settings/header.html %] - + [% WRAPPER setting title="SETUP_DBHIGHMEM" desc="SETUP_DBHIGHMEM_DESC" %] [% END %] - + [% WRAPPER setting title="SETUP_SCAN_ON_PREF_CHANGE" desc="SETUP_SCAN_ON_PREF_CHANGE_DESC" %] [% END %] - + [% WRAPPER setting title="SETUP_PRECACHEARTWORK" desc="SETUP_PRECACHEARTWORK_DESC" %] [% END %] @@ -44,7 +44,7 @@ - + [% FOREACH pref = pref_customArtSpecs %] @@ -53,7 +53,7 @@ [% END %] - +
[% pref.value %]
[% END; END %] @@ -63,35 +63,35 @@ [% FOREACH id = imageproxies.keys.sort %] [% END %] - + [% END %] - [% WRAPPER settingSection %] + [% IF prioritySettings; WRAPPER settingSection %] [% WRAPPER settingGroup title="SETUP_SERVERPRIORITY" desc="SETUP_SERVERPRIORITY_DESC" %] [% END %] [% WRAPPER settingGroup title="SETUP_SCANNERPRIORITY" desc="SETUP_SCANNERPRIORITY_DESC" %] [% END %] - [% END %] + [% END; END %] [% IF prefs.exists('pref_autorescan') %] [% WRAPPER setting title="SETUP_AUTO_RESCAN" desc="SETUP_AUTO_RESCAN_DESC" %] @@ -105,7 +105,7 @@ [% END %] [% END %] - + [% IF prefs.exists('pref_autorescan_stat_interval') %] [% WRAPPER setting title="SETUP_AUTO_RESCAN_STAT_INTERVAL" desc="SETUP_AUTO_RESCAN_STAT_INTERVAL_DESC" %] diff --git a/HTML/EN/settings/server/security.html b/HTML/EN/settings/server/security.html index ed26c9d03a9..241bb1582cc 100644 --- a/HTML/EN/settings/server/security.html +++ b/HTML/EN/settings/server/security.html @@ -3,10 +3,10 @@ [% WRAPPER settingSection %] [% WRAPPER settingGroup title="SETUP_AUTHORIZE" desc="SETUP_AUTHORIZE_DESC" %] [% END %] @@ -26,10 +26,10 @@ [% WRAPPER settingSection %] [% WRAPPER settingGroup title="SETUP_IPFILTER_HEAD" desc="SETUP_IPFILTER_DESC" %] [% END %] @@ -39,14 +39,17 @@ [% WRAPPER settingGroup title="SETUP_CSRFPROTECTIONLEVEL" desc="SETUP_CSRFPROTECTIONLEVEL_DESC" %] [% END %] - [% END %] + [% WRAPPER settingGroup title="SETUP_CORS_ALLOWED_HOSTS" desc="SETUP_CORS_ALLOWED_HOSTS_DESC" %] + + [% END %] + [% END %] [% PROCESS settings/footer.html %] diff --git a/HTML/EN/songinfo_header.html b/HTML/EN/songinfo_header.html index 08d18ec9c60..03b8875f03d 100644 --- a/HTML/EN/songinfo_header.html +++ b/HTML/EN/songinfo_header.html @@ -7,7 +7,7 @@ [% IF image || plugin_meta.cover %] [% UNLESS image.match('^http'); image = image.replace('^/?', webroot); END %] - coverArt + coverArt [% ELSIF plugin_meta.icon %] coverArt diff --git a/HTML/EN/status.html b/HTML/EN/status.html index 11771f74dd5..c9562e5e5f8 100644 --- a/HTML/EN/status.html +++ b/HTML/EN/status.html @@ -16,7 +16,7 @@

[% playermodel _ "_MUSIC_PLAYER" | string %] [% IF player_name %][% player_n [% IF currentsong %][% currentsong %] [% "OUT_OF" | string %] [% songcount %][% END %][% stringCOLON %]
[% songtitle %] - [% IF itemobj.album.title && itemobj.album.title != noAlbum %] + [% IF itemobj.album.title.defined && itemobj.album.title != noAlbum %] [% stringFROM %] [% itemobj.album.title | html %] [%- END %] [% IF itemobj.artist && itemobj.artist != noArtist %] diff --git a/HTML/EN/status_list.html b/HTML/EN/status_list.html index b26cd9eb080..cfe5258f7dd 100644 --- a/HTML/EN/status_list.html +++ b/HTML/EN/status_list.html @@ -7,7 +7,7 @@ [% END %] [% item.title | html %] - [% IF item.includeAlbum && item.album && item.album != noAlbum %] + [% IF item.includeAlbum && item.album.defined && item.album != noAlbum %] [% stringFROM %] [% IF item.album_id %] [% item.album | html %] diff --git a/HTML/EN/update_software.html b/HTML/EN/update_software.html index c20968478ae..239e191717f 100644 --- a/HTML/EN/update_software.html +++ b/HTML/EN/update_software.html @@ -87,10 +87,10 @@ callbackKey: "callback", callback: function(c) { var el = Ext.get('changelogWrapper'); - + if (!el) return; - + var xt = new Ext.XTemplate( '', '
{[this.previousDate = values.date]}
', @@ -111,7 +111,7 @@ url: item.html_url }); }); - + el.setDisplayed('block'); } }); @@ -119,35 +119,34 @@ [% END %] [% PROCESS pageheader.html %] -
- +
+ [% IF newVersion %]

[% "CONTROLPANEL_UPDATE_AVAILABLE" | string %]

- +
[% newVersion %]
- + - +
[% "MORE" | string %]
[% ELSE %]

[% "CONTROLPANEL_NO_UPDATE_AVAILABLE" | string | html %]

[% END %] - +
 
- - [% IF newPlugins && newPlugins.keys.count %] + [% IF newPlugins && newPlugins.size %]

[% "PLUGINS_UPDATES_AVAILABLE" | string %]

[% FOREACH plugin = newPlugins %] -
[% plugin.title _ " (" _ plugin.version _ ")" -%]
+
[% plugin.title _ " (" _ plugin.version _ ")" -%]
[%- IF plugin.changes %]
[% plugin.changes %]
[% END %] [% END %]
[% END %] - +
[% PROCESS pagefooter.html %] \ No newline at end of file diff --git a/HTML/EN/xmlbrowser.html b/HTML/EN/xmlbrowser.html index c8b8c8d74cf..d5a70ee7971 100644 --- a/HTML/EN/xmlbrowser.html +++ b/HTML/EN/xmlbrowser.html @@ -72,7 +72,7 @@ [% PROCESS pagebar %] [% END %] - [% BLOCK favoritescontrol %] + [% BLOCK favoritescontrol %] [% IF item.favorites == 1 %] [% WRAPPER favaddlink noTarget=1 %] [%- IF useAJAX -%]href="javascript:void(0);" onClick="ajaxUpdate('[% path %]', 'action=favadd&index=[% item.index || index _ (start + loop.index) %]&start=[% pageinfo.startitem %]&sess=[% sess %]')" @@ -223,10 +223,12 @@ + [% ELSIF item.type == 'textarea' %] + [% title = (item.web.value || item.name || item.title) %] [% title | html_line_break %] [% ELSE %] [% IF useAJAX %] + onMouseOver="showElements(['controls[% item.index || index _ ((start || 0) + loop.index) %]'], 'inline');" + onMouseOut="hideElements(['controls[% item.index || index _ ((start || 0) + loop.index) %]'])"> [% END %] [%- WRAPPER $contentwrapper leftcontrols = 'gencontrol' rightcontrols = (playlist_id ? 'editcontrols' : 'favoritescontrol') anchor = item.anchor %] @@ -251,9 +253,13 @@ [% ELSIF item.simpleAlbumLink %] [% ELSIF !item.type.match('^text') %] - + [% END %] - [% item.name || item.title || item.web.value %] + [% title = item.web.value; + IF !title.defined || title == ''; title = item.name; END; + IF !title.defined || title == ''; title = item.title; END + %] + [% title | html | html_line_break %] [% IF item.weblink || !item.type.match('^text') %] [% END %] diff --git a/IR/Default.map b/IR/Default.map index a64c721f09a..2f35d2ad7ae 100644 --- a/IR/Default.map +++ b/IR/Default.map @@ -98,18 +98,26 @@ trebleup.repeat = treble_up volume = volume volumemode = volumemode home = home +preset_0.single = playPreset_0 preset_1.single = playPreset_1 preset_2.single = playPreset_2 preset_3.single = playPreset_3 preset_4.single = playPreset_4 preset_5.single = playPreset_5 preset_6.single = playPreset_6 +preset_7.single = playPreset_7 +preset_8.single = playPreset_8 +preset_9.single = playPreset_9 +preset_0.hold = favorites_add0 preset_1.hold = favorites_add1 preset_2.hold = favorites_add2 preset_3.hold = favorites_add3 preset_4.hold = favorites_add4 preset_5.hold = favorites_add5 preset_6.hold = favorites_add6 +preset_7.hold = favorites_add7 +preset_8.hold = favorites_add8 +preset_9.hold = favorites_add9 # playing display modes now_playing = playdisp_toggle diff --git a/MySQL/my-highmem.tt b/MySQL/my-highmem.tt index 2b0cac99e1a..926cb2d1f3c 100644 --- a/MySQL/my-highmem.tt +++ b/MySQL/my-highmem.tt @@ -1,5 +1,3 @@ -# $Id$ -# # Logitech Media Server specific MySQL Server config. # High-memory configuration by Moonbase # http://forums.slimdevices.com/showthread.php?t=60682 diff --git a/MySQL/my.tt b/MySQL/my.tt index a32aab58506..30cee080efb 100644 --- a/MySQL/my.tt +++ b/MySQL/my.tt @@ -1,5 +1,3 @@ -# $Id$ -# # Logitech Media Server specific MySQL Server config. [mysqld] diff --git a/README.md b/README.md new file mode 100644 index 00000000000..feeea4a4a00 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +Logitech Media Server +==== + +Logitech Media Server (aka. LMS, fka. SlimServer, SqueezeCenter, SqueezeboxServer, SliMP3) is the server software that powers audio players from [Logitech](https://www.logi.com) (formerly known as SlimDevices), including [Squeezebox 3rd Generation, Squeezebox Boom, Squeezebox Receiver, Transporter, Squeezebox2, Squeezebox and SLIMP3](http://wiki.slimdevices.com/index.php/Squeezebox_Family_Overview), and many software emulators like [Squeezelite and SqueezePlay](https://sourceforge.net/projects/lmsclients/files/). + +With the help of many plugins, Logitech Media Server can stream not only your local music collection, but content from many music services and internet radio stations to your players. + +Logitech Media Server is written in Perl. It runs on pretty much any platform that Perl runs on, including Linux, Mac OSX, Solaris and Windows. \ No newline at end of file diff --git a/SOCKS.txt b/SOCKS.txt new file mode 100644 index 00000000000..4c7d527b639 --- /dev/null +++ b/SOCKS.txt @@ -0,0 +1,75 @@ +SSH tunnel with port forwarding allows per port TCP/UDP traffic to be +"forwarded" from one end to the tunnel to the other and appear like it was +initiated from the remote end of tunnel. For example all traffic send to +port 5000 on the local machine can be forwared to a remote machine, on port +25000 and will appear to any client on the remote side as if it was locally +coming from port 25000 + +SOCKS is a type of proxy server that works at the TCP level. It usually sits +on port 1080 and forward TCP traffic from there. Because it works at the TCP +level (contrary to classical HTTP proxies), it can be coupled to SSH tunnels +to create very convenient firewall passthrough and geo-locked services unlocking. +This tunneling is not a all-or-nothing tunnel an allow each TCP request to be +selectively forwarding or not + +You must first either use a public SOCKS server or create your own SOCKS/SSH +pair in which ase you must have a local SOCKS server, a local SSH client and +a remote SSH server. On Linux openssh does everything, one local instance with +dynamic port forwarding (-D) and a remote instance to a friend's network does +the job. On Windows, you can use Bitvise Client & Server. There is plenty of +internet litterature on SOCKS/SSH that explain the concept much better than +anything I could write :-) + +One thing to notice with SOCKS5 is the lack of proper authentication mechanism +which means that if you have a username/password, they will be sent in clear +to the server. SOCKS4 does not require authentication. That's why I prefer +to have a local SOCKS server that creates a SSH tunnel to a remote end but this +means you must have a SSH remote end. + +To use socks proxy to your HTTP requests, set the SOCKS parameter in LMS and +simply passed a hash named "socks" to SimpleAsyncHTTP::new or Async::HTP::new +with the following content (see IO::Socket::Socks) + + my $http = Slim::Networking::SimpleAsyncHTTP->new( + sub { # succes CB }, + sub { # error CB }, + { + socks => { + ProxyAddr => '192.168.0.1' + ProxyPort => 1080, + Username => 'user', + Password => 'password', + } + } + ); + + $http->get("http://www.google.com") + + - or - + + my $http = Slim::Networking::Async::HTTP->new( { + socks => { + ProxyAddr => '192.168.0.1' + ProxyPort => 1080, + Username => 'user', + Password => 'password', + } + } + ); + $http ->send_request( { + 'request' => HTTP::Request->new( GET => "http://www.google.com" ), + 'onHeaders' => sub { }, + 'onError' => sub { }, + 'passthrough' => [ $p1, $p2 ], + } + ); + +Note that Username and Password are optional. SOCKS version is set to 5 if they +are set and to 4 otherwise. ProxyPort can be omitted and will be set to 1080. + +If 'socks' hash is set but ProxyAddr is missing, a regular SimpleAsync or Async +call will be made + + + + \ No newline at end of file diff --git a/SQL/SQLite/schema_clear.sql b/SQL/SQLite/schema_clear.sql index 8dbbb71ec38..c3a6a9c571e 100644 --- a/SQL/SQLite/schema_clear.sql +++ b/SQL/SQLite/schema_clear.sql @@ -35,4 +35,8 @@ DELETE FROM library_album; DELETE FROM library_contributor; DELETE FROM library_genre; +-- these table are created by the Fulltext Search plugin +DROP TABLE IF EXISTS fulltext; +DROP TABLE IF EXISTS fulltext_terms; + UPDATE metainformation SET value = 0 WHERE name = 'lastRescanTime'; diff --git a/Slim/Buttons/Alarm.pm b/Slim/Buttons/Alarm.pm index a22734f13db..e94cfeeba29 100644 --- a/Slim/Buttons/Alarm.pm +++ b/Slim/Buttons/Alarm.pm @@ -4,7 +4,7 @@ use strict; # Max Spicer, May 2008 # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Block.pm b/Slim/Buttons/Block.pm index f187c7857bb..7f64c65a2dd 100644 --- a/Slim/Buttons/Block.pm +++ b/Slim/Buttons/Block.pm @@ -1,6 +1,6 @@ package Slim::Buttons::Block; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Common.pm b/Slim/Buttons/Common.pm index 920efe86cd1..2dfa5c3cfbf 100644 --- a/Slim/Buttons/Common.pm +++ b/Slim/Buttons/Common.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Common; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/GlobalSearch.pm b/Slim/Buttons/GlobalSearch.pm index e5eead33773..ab55134aed6 100644 --- a/Slim/Buttons/GlobalSearch.pm +++ b/Slim/Buttons/GlobalSearch.pm @@ -2,7 +2,7 @@ package Slim::Buttons::GlobalSearch; # $Id: Information.pm 26931 2009-06-07 03:53:36Z michael $ # -# Copyright (c) 2003-2009 Logitech, Cursor Software Limited. +# Copyright (c) 2003-2020 Logitech, Cursor Software Limited. # All rights reserved. # # This program is free software; you can redistribute it and/or modify diff --git a/Slim/Buttons/Home.pm b/Slim/Buttons/Home.pm index 1d42d479f4f..4fe330d17a8 100644 --- a/Slim/Buttons/Home.pm +++ b/Slim/Buttons/Home.pm @@ -1,6 +1,6 @@ package Slim::Buttons::Home; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Information.pm b/Slim/Buttons/Information.pm index 5ef551e2b95..fe975a01551 100644 --- a/Slim/Buttons/Information.pm +++ b/Slim/Buttons/Information.pm @@ -3,7 +3,7 @@ package Slim::Buttons::Information; # $Id$ # # Author: Kevin Walsh -# Copyright (c) 2003-2009 Logitech, Cursor Software Limited. +# Copyright (c) 2003-2020 Logitech, Cursor Software Limited. # All rights reserved. # # This program is free software; you can redistribute it and/or modify diff --git a/Slim/Buttons/Input/Bar.pm b/Slim/Buttons/Input/Bar.pm index f7fb3593e43..1ee50fbf6e3 100644 --- a/Slim/Buttons/Input/Bar.pm +++ b/Slim/Buttons/Input/Bar.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Input::Bar; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Input/Choice.pm b/Slim/Buttons/Input/Choice.pm index 51f3527709b..6c9ef987d1d 100644 --- a/Slim/Buttons/Input/Choice.pm +++ b/Slim/Buttons/Input/Choice.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Input::Choice; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -29,7 +28,7 @@ Slim::Buttons::Input::Choice onAdd => sub { }, - + onRight => $client->modeParam('onRight'), # passthrough ); @@ -61,23 +60,8 @@ my $prefs = preferences('server'); my $log = logger('player.ui'); -# TODO: move browseCache into Client object, where it will be cleaned up after client is forgotten -our %browseCache = (); # remember where each client is browsing - Slim::Buttons::Common::addMode('INPUT.Choice', getFunctions(), \&setMode); -=head2 forgetClient ( $client ) - -Clean up global hash when a client is gone - -=cut - -sub forgetClient { - my $client = shift; - - delete $browseCache{ $client }; -} - # get the value the user is currently referencing. # item could be hash, string or code sub getItem { @@ -100,7 +84,7 @@ sub getItem { sub getItemName { my $client = shift; my $index = shift; # optional - + # if name has been overridden by a function, this code will get that. # A 'name' function in the item is preferred over a 'name' modeParam @@ -108,7 +92,7 @@ sub getItemName { if ( ref($item) && $item->{'name'} && ref $item->{'name'} eq 'CODE' ) { return $item->{'name'}; } - + if (my $name = $client->modeParam('name')) { return $name; } @@ -121,7 +105,7 @@ sub getItemName { if ( ref($item) && $item->{'name'}) { return $item->{'name'}; } - + return $item; } @@ -132,10 +116,10 @@ sub getItemValue { my $item = getItem($client, $index); if (ref($item)) { - return $item->{'value'}; + return defined $item->{'value'} ? $item->{'value'} : ''; } - return $item; + return defined $item ? $item : ''; } # some values can be mode-wide, or overridden at the list item level @@ -172,7 +156,7 @@ sub getExtVal { if ($@) { logError("Couldn't run coderef. [$@]"); - + return ''; } @@ -205,7 +189,7 @@ my %functions = ( my ($client,$funct,$functarg) = @_; my ($newPos, $dir, $pushDir, $wrap) = $client->knobListPos(); - + changePos($client, $dir, $funct, $pushDir) if $pushDir; }, 'numberScroll' => sub { @@ -258,7 +242,7 @@ my %functions = ( 'play' => sub { callCallback('onPlay', @_) }, 'add' => sub { callCallback('onAdd', @_) }, 'create_mix' => sub { callCallback('onCreateMix', @_) }, - + # right and left buttons is handled in exitInput # add more explicit callbacks if necessary here. @@ -302,9 +286,9 @@ sub callCallback { if ($@) { logError("Couldn't run callback: [$callbackName] : $@"); - + } elsif (getParam($client,'pref')) { - + $client->update; } @@ -320,15 +304,15 @@ sub changePos { my $listRef = $client->modeParam('listRef'); my $listIndex = $client->modeParam('listIndex'); - + if ($client->modeParam('noWrap')) { - + #not wrapping and at end of list if ($listIndex == 0 && $dir < 0) { $client->bumpUp() if ($funct !~ /repeat/); return; } - + if ($listIndex >= (scalar(@$listRef) - 1) && $dir > 0) { $client->bumpDown() if ($funct !~ /repeat/); return; @@ -336,13 +320,13 @@ sub changePos { } my $newposition = Slim::Buttons::Common::scroll($client, $dir, scalar(@$listRef), $listIndex); - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "newpos: $newposition = scroll dir:$dir listIndex: $listIndex listLen: ", scalar(@$listRef) ); } - + my $valueRef = $client->modeParam('valueRef'); $$valueRef = $listRef->[$newposition]; @@ -365,23 +349,23 @@ sub changePos { } } elsif ($newposition != $listIndex) { - + $pushDir ||= ''; - + if ($pushDir eq 'up') { - + $client->pushUp(); } elsif ($pushDir eq 'down') { - + $client->pushDown(); } elsif ($dir < 0) { - + $client->pushUp(); } else { - + $client->pushDown(); } @@ -396,13 +380,13 @@ sub changePos { $value = $value->{'value'}; } - $browseCache{$client}{$client->modeParam("modeName")} = $value; + $client->browseCache->{$client->modeParam("modeName")} = $value; } } # callers can specify strings (i.e. header) as a string like this... # text text text {STRING1} text {count} text {STRING2} -# and the behavior will be +# and the behavior will be # 'text' will go through unchanged # '{STRING}' will be replaced with STRING translated # '{count}' is stripped out as it is now shown in the overlay (modeParam headerAddCount is preferred) @@ -467,9 +451,9 @@ sub lines { if (ref($overlayref) eq 'ARRAY') { ($overlay1, $overlay2) = @$overlayref; - + } elsif (my $pref = getParam($client,'pref')) { - + # assume a single non-descending list of items, 'pref' item must be given in the params my $val = ref $pref eq 'CODE' ? $pref->($client) : preferences('server')->client($client)->get($pref); if (scalar(@$listRef) == 1) { @@ -560,11 +544,11 @@ sub init { if ($setMethod eq 'push') { my $initialValue = getExtVal($client, getParam($client, 'initialValue')); - + # if initialValue not provided, use the one we saved if (!$initialValue && $client->modeParam("modeName")) { - $initialValue = $browseCache{$client}{$client->modeParam("modeName")}; + $initialValue = $client->browseCache->{$client->modeParam("modeName")}; } if ($initialValue) { @@ -622,7 +606,7 @@ sub exitInput { }, undef, 1); } elsif (getParam($client,'pref')) { - + $client->update; } diff --git a/Slim/Buttons/Input/List.pm b/Slim/Buttons/Input/List.pm index 6402ff7c978..4ea9ada221e 100644 --- a/Slim/Buttons/Input/List.pm +++ b/Slim/Buttons/Input/List.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Input::List; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Input/Text.pm b/Slim/Buttons/Input/Text.pm index 52075113f4d..67b0a546086 100644 --- a/Slim/Buttons/Input/Text.pm +++ b/Slim/Buttons/Input/Text.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Input::Text; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Input/Time.pm b/Slim/Buttons/Input/Time.pm index 3ac77d54562..3391adc9d9f 100644 --- a/Slim/Buttons/Input/Time.pm +++ b/Slim/Buttons/Input/Time.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Input::Time; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Playlist.pm b/Slim/Buttons/Playlist.pm index 1d6fb03c694..4437f1a0140 100644 --- a/Slim/Buttons/Playlist.pm +++ b/Slim/Buttons/Playlist.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Playlist; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Power.pm b/Slim/Buttons/Power.pm index c37a7153971..a053ddb1973 100644 --- a/Slim/Buttons/Power.pm +++ b/Slim/Buttons/Power.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Power; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/RemoteTrackInfo.pm b/Slim/Buttons/RemoteTrackInfo.pm index c2d7ff41462..454a76eeef3 100644 --- a/Slim/Buttons/RemoteTrackInfo.pm +++ b/Slim/Buttons/RemoteTrackInfo.pm @@ -1,8 +1,7 @@ package Slim::Buttons::RemoteTrackInfo; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/ScreenSaver.pm b/Slim/Buttons/ScreenSaver.pm index 549831435bb..c938f5f9d49 100644 --- a/Slim/Buttons/ScreenSaver.pm +++ b/Slim/Buttons/ScreenSaver.pm @@ -1,8 +1,7 @@ package Slim::Buttons::ScreenSaver; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Search.pm b/Slim/Buttons/Search.pm deleted file mode 100644 index e2c91fa7189..00000000000 --- a/Slim/Buttons/Search.pm +++ /dev/null @@ -1,270 +0,0 @@ -package Slim::Buttons::Search; - -# XXX - this package is obsolete. It's no longer being loaded, as its functionality has been re-implemented in Slim::Menu::BrowseLibrary. - mh - -# Logitech Media Server Copyright 2001-2011 Logitech. -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, -# version 2. - -=head1 NAME - -Slim::Buttons::Search - -=head1 DESCRIPTION - -L is a Logitech Media Server module to create a UI for searching -the user track database. Seach by ARTIST, ALBUM and SONGS is added to the home -menu structure as well as options for adding to the top level. Search input uses the -INPUT.Text mode. - -=cut - -use strict; -use Slim::Buttons::Common; -use Slim::Utils::Prefs; - -# button functions for search directory -my @defaultSearchChoices = qw(ARTISTS ALBUMS SONGS); - -our %context = (); -our %menuParams = (); - -sub setMode { - my $client = shift; - my $method = shift; - - if ($method eq 'pop') { - Slim::Buttons::Common::popMode($client); - return; - } - - #grab the top level search parameters - my %params = %{$menuParams{'SEARCH'}}; - - Slim::Buttons::Common::pushMode($client,'INPUT.List',\%params); - $client->update(); -} - -sub init { - Slim::Buttons::Common::addMode('search',{}, \&Slim::Buttons::Search::setMode); - - my %subs = ( - - 'SEARCH_FOR_ARTISTS' => sub { - return Slim::Buttons::Search::searchFor(shift, 'ARTISTS'); - }, - - 'SEARCH_FOR_ALBUMS' => sub { - return Slim::Buttons::Search::searchFor(shift, 'ALBUMS'); - }, - - 'SEARCH_FOR_SONGS' => sub { - return Slim::Buttons::Search::searchFor(shift, 'SONGS'); - } - ); - - # - %menuParams = ( - 'SEARCH' => { - 'listRef' => \@defaultSearchChoices, - 'stringExternRef' => 1, - 'header' => 'SEARCH', - 'stringHeader' => 1, - 'headerAddCount' => 1, - 'callback' => \&searchExitHandler, - 'overlayRef' => sub { return (undef, shift->symbols('rightarrow')) }, - 'overlayRefArgs' => 'C', - 'submenus' => { - - 'ARTISTS' => { - 'useMode' => 'INPUT.Text', - 'header' => 'SEARCHFOR_ARTISTS', - 'stringHeader' => 1, - 'cursorPos' => 0, - 'charsRef' => 'UPPER', - 'numberLetterRef' => 'UPPER', - 'callback' => \&searchHandler, - }, - - 'ALBUMS' => { - 'useMode' => 'INPUT.Text', - 'header' => 'SEARCHFOR_ALBUMS', - 'stringHeader' => 1, - 'cursorPos' => 0, - 'charsRef' => 'UPPER', - 'numberLetterRef' => 'UPPER', - 'callback' => \&searchHandler, - }, - - 'SONGS' => { - 'useMode' => 'INPUT.Text', - 'header' => 'SEARCHFOR_SONGS', - 'stringHeader' => 1, - 'cursorPos' => 0, - 'charsRef' => 'UPPER', - 'numberLetterRef' => 'UPPER', - 'callback' => \&searchHandler, - } - } - } - ); - - for my $name (sort keys %menuParams) { - Slim::Buttons::Home::addSubMenu('BROWSE_MUSIC', $name, $menuParams{$name}); - Slim::Buttons::Home::addMenuOption($name,$menuParams{$name}); - } - - for my $name (sort keys %subs) { - Slim::Buttons::Home::addMenuOption($name,$subs{$name}); - } -} - -=head2 forgetClient ( $client ) - -Clean up global hash when a client is gone - -=cut - -sub forgetClient { - my $client = shift; - - delete $context{ $client }; -} - -sub searchExitHandler { - my ($client,$exitType) = @_; - - $exitType = uc($exitType); - - if ($exitType eq 'LEFT') { - - Slim::Buttons::Common::popModeRight($client); - - } elsif ($exitType eq 'RIGHT') { - - my $current = $client->modeParam('valueRef'); - - my %nextParams = searchFor($client, $$current) ; - - Slim::Buttons::Common::pushModeLeft($client, $nextParams{'useMode'}, \%nextParams); - } -} - -sub searchFor { - my $client = shift; - my $search = shift; - my $value = shift; - - $context{$client} = (defined($value) && length($value)) ? ($value) : ('A'); - - my %nextParams = %{$menuParams{'SEARCH'}{'submenus'}{$search}}; - - $nextParams{'valueRef'} = \$context{$client}; - - $client->searchFor($search); - - return %nextParams; -} - -sub searchHandler { - my ($client,$exitType) = @_; - - $exitType = uc($exitType); - - if ($exitType eq 'BACKSPACE') { - Slim::Buttons::Common::popModeRight($client); - } else { - startSearch($client); - } -} - -sub startSearch { - my $client = shift; - my $mode = shift; - my $oldlines = $client->curLines(); - - my $term = searchTerm($client); - $client->showBriefly( { - 'line' => [ $client->string('SEARCHING'), undef ] - }); - - if ($client->searchFor eq 'ARTISTS') { - - Slim::Buttons::Common::pushMode($client, 'browsedb', { - 'search' => $term, - 'hierarchy' => 'contributor,album,track', - 'level' => 0, - }); - - } elsif ($client->searchFor eq 'ALBUMS') { - - Slim::Buttons::Common::pushMode($client, 'browsedb', { - 'search' => $term, - 'hierarchy' => 'album,track', - 'level' => 0, - }); - - } else { - - Slim::Buttons::Common::pushMode($client, 'browsedb', { - 'search' => $term, - 'hierarchy' => 'track', - 'level' => 0, - }); - } - - $client->pushLeft($oldlines, $client->curLines()); -} - -sub searchTerm { - my $client = shift; - my $search = shift; - - $search = $context{$client} if !defined $search; - - # do the search! - @{$client->searchTerm} = split(//, Slim::Utils::Text::ignoreCase($search)); - - my $term = ''; - - my $prefs = preferences('server'); - - # Bug #738 - # Which should be the default? Old - which is substring always? - if ($prefs->get('searchSubString')) { - $term = '%'; - } - - for my $a (@{$client->searchTerm}) { - - if (defined($a)) { - $term .= $a; - } - } - - $term .= '%'; - - # If we're searching in substrings, return - otherwise append another - # search which is effectively \b for the query. We might (should?) - # deal with alternate separator characters other than space. - if ($prefs->get('searchSubString')) { - return [ $term ]; - } - - return [ $term, "% $term" ]; -} - -=head1 SEE ALSO - -L - -L - -L - -=cut - -1; - -__END__ diff --git a/Slim/Buttons/Settings.pm b/Slim/Buttons/Settings.pm index 87a049286f8..01babee2c56 100644 --- a/Slim/Buttons/Settings.pm +++ b/Slim/Buttons/Settings.pm @@ -1,6 +1,6 @@ package Slim::Buttons::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/SqueezeNetwork.pm b/Slim/Buttons/SqueezeNetwork.pm index 78ab8587fe7..3a8ccbf953d 100644 --- a/Slim/Buttons/SqueezeNetwork.pm +++ b/Slim/Buttons/SqueezeNetwork.pm @@ -2,7 +2,7 @@ package Slim::Buttons::SqueezeNetwork; # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2006-2011 Logitech. +# Logitech Media Server Copyright 2006-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Synchronize.pm b/Slim/Buttons/Synchronize.pm index 7f819ed343f..d53962fbbbb 100644 --- a/Slim/Buttons/Synchronize.pm +++ b/Slim/Buttons/Synchronize.pm @@ -1,8 +1,7 @@ package Slim::Buttons::Synchronize; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/TrackInfo.pm b/Slim/Buttons/TrackInfo.pm index 3f55c6b2194..52a4dbc77cf 100644 --- a/Slim/Buttons/TrackInfo.pm +++ b/Slim/Buttons/TrackInfo.pm @@ -1,6 +1,6 @@ package Slim::Buttons::TrackInfo; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/Volume.pm b/Slim/Buttons/Volume.pm index 7d1d7cd4475..29f50b29427 100644 --- a/Slim/Buttons/Volume.pm +++ b/Slim/Buttons/Volume.pm @@ -1,8 +1,6 @@ package Slim::Buttons::Volume; -# $Id$ -# -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Buttons/XMLBrowser.pm b/Slim/Buttons/XMLBrowser.pm index afdad6076a1..611085d41cd 100644 --- a/Slim/Buttons/XMLBrowser.pm +++ b/Slim/Buttons/XMLBrowser.pm @@ -1,8 +1,6 @@ package Slim::Buttons::XMLBrowser; -# $Id$ - -# Copyright 2005-2009 Logitech. +# Logitech Media Server Copyright 2005-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, @@ -326,7 +324,7 @@ sub gotRSS { my ($client, $url, $feed, $params) = @_; # Include an item to access feed info - if (($feed->{'items'}->[0]->{'value'} ne 'description') && + if (($feed->{'items'}->[0]->{'value'} && $feed->{'items'}->[0]->{'value'} ne 'description') && # skip this if xmlns:slim is used, and no description found !($feed->{'xmlns:slim'} && !$feed->{'description'})) { @@ -537,6 +535,7 @@ sub gotOPML { # keep track of station icons if ( ( $item->{play} || $item->{playlist} || ($item->{type} && ($item->{type} eq 'audio' || $item->{type} eq 'playlist')) ) + && $item->{url} && !ref $item->{url} && $item->{url} =~ /^http/ && $item->{url} !~ m|\.com/api/\w+/v1/opml| && ( my $cover = $item->{image} || $item->{cover} ) @@ -1238,7 +1237,7 @@ sub playItem { $type = 'playlist'; } - main::DEBUGLOG && $log->debug("Playing item, action: $action, type: $type, $url"); + main::DEBUGLOG && $log->is_debug && $log->debug("Playing item, action: $action, type: $type, $url"); my $playalbum = $prefs->client($client)->get('playtrackalbum'); diff --git a/Slim/Control/Commands.pm b/Slim/Control/Commands.pm index 36d05f92701..30242ac7a11 100644 --- a/Slim/Control/Commands.pm +++ b/Slim/Control/Commands.pm @@ -1,8 +1,6 @@ package Slim::Control::Commands; -# $Id: Commands.pm 5121 2005-11-09 17:07:36Z dsully $ -# -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -50,7 +48,7 @@ sub abortScanCommand { my $request = shift; Slim::Music::Import->abortScan(); - + $request->setStatusDone(); } @@ -65,13 +63,13 @@ sub alarmCommand { # Please check other commands for examples of more advanced usage of # isNotCommand for multi-term commands if ($request->isNotCommand([['alarm']])) { - + # set an appropriate error state. This will stop execute and callback # and notification, etc. $request->setStatusBadDispatch(); return; } - + # get the parameters. In this case the parameters are defined here only # but for some commands, the parameters name start with _ and are defined # in the big dispatch table (see Request.pm). @@ -106,7 +104,7 @@ sub alarmCommand { $request->setStatusBadParams(); return; } - + # required param for 'defaultvolume' is volume if ( $params->{cmd} eq 'defaultvolume' && ! defined $params->{volume} ) { $request->setStatusBadParams(); @@ -116,13 +114,13 @@ sub alarmCommand { # required param for 'add' is time, given as numbers only # client needs to be given # dow needs to be properly formatted - if ( $params->{cmd} eq 'add' && + if ( $params->{cmd} eq 'add' && ( ! defined $params->{time} || $params->{time} =~ /\D/ || ! defined $client || ( defined $params->{dow} && $params->{dow} !~ /^[0-6](?:,[0-6])*$/ ) - ) + ) ) { $request->setStatusBadParams(); return; @@ -141,7 +139,7 @@ sub alarmCommand { } my $alarm; - + if ($params->{cmd} eq 'add') { $alarm = Slim::Utils::Alarm->new($client); } @@ -166,7 +164,7 @@ sub alarmCommand { } else { - + $alarm->time($params->{time}) if defined $params->{time}; # the playlisturl param is supported for backwards compatability # but url is preferred @@ -195,11 +193,11 @@ sub alarmCommand { # handle dow tag, if defined if ( defined $params->{dow} ) { foreach (0..6) { - my $set = $params->{dow} =~ /$_/; + my $set = $params->{dow} =~ /$_/; $alarm->day($_, $set); } } - + # allow for a dowAdd and dowDel param for adding/deleting individual days # these directives take precendence over anything that's in dow if ( defined $params->{dowAdd} ) { @@ -208,7 +206,7 @@ sub alarmCommand { if ( defined $params->{dowDel} ) { $alarm->day($params->{dowDel}, 0); } - + $alarm->save(); } @@ -217,7 +215,7 @@ sub alarmCommand { # likely the CLI). $request->addResult('id', $alarm->id); } - + # indicate the request is done. This enables execute to continue with # calling the callback and notifying, etc... $request->setStatusDone(); @@ -226,7 +224,7 @@ sub alarmCommand { sub artworkspecCommand { my $request = shift; - + # get the parameters my $name = $request->getParam('_name') || ''; my $spec = $request->getParam('_spec'); @@ -236,9 +234,9 @@ sub artworkspecCommand { $request->setStatusBadDispatch(); return; } - + main::DEBUGLOG && $log->debug("Registering artwork resizing spec: $spec ($name)"); - + # do some sanity checking my ($width, $height, $mode, $bgcolor, $ext) = Slim::Web::Graphics->parseSpec($spec); if ($width && $height && $mode) { @@ -252,13 +250,13 @@ sub artworkspecCommand { elsif ( !$oldName && !(scalar grep /$spec/, Slim::Music::Artwork::getResizeSpecs()) ) { $specs->{$spec} = $name; } - + $prefs->set('customArtSpecs', $specs); } else { $log->error('Invalid artwork resizing specification: ' . $spec); } - + $request->setStatusDone(); } @@ -271,25 +269,25 @@ sub buttonCommand { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); my $button = $request->getParam('_buttoncode'); my $time = $request->getParam('_time'); my $orFunction = $request->getParam('_orFunction'); - + if (!defined $button ) { $request->setStatusBadParams(); return; } - + # if called from cli, json or comet then store this as the last button as we have bypassed the ir code if ($request->source) { $client->lastirbutton($button); } Slim::Hardware::IR::executeButton($client, $button, $time, undef, defined($orFunction) ? $orFunction : 1); - + $request->setStatusDone(); } @@ -301,12 +299,12 @@ sub clientConnectCommand { if ( $client->hasServ() ) { my ($host, $packed); $host = $request->getParam('_where'); - + # Bug 14224, if we get jive/baby/fab4.squeezenetwork.com, use the configured prod SN hostname if ( !main::NOMYSB && $host =~ /^(?:jive|baby|fab4)/i ) { $host = Slim::Networking::SqueezeNetwork->get_server('sn'); } - + if ( !main::NOMYSB && $host =~ /^www\.(?:squeezenetwork|mysqueezebox)\.com$/i ) { $host = 1; } @@ -322,32 +320,32 @@ sub clientConnectCommand { } else { $host = Slim::Utils::Network::intip($host); - + if ( !$host ) { $request->setStatusBadParams(); return; } } - + if ($client->controller()->allPlayers() > 1) { - my $syncgroupid = $prefs->client($client)->get('syncgroupid') || 0; + my $syncgroupid = $prefs->client($client)->get('syncgroupid') || 0; $packed = pack 'NA10', $host, sprintf('%010d', $syncgroupid); } else { $packed = pack 'N', $host; } - + $client->execute([ 'stop' ]); - + foreach ($client->controller()->allPlayers()) { - + if ($_->hasServ()) { $_->sendFrame( serv => \$packed ); - + # Bug 14400: make sure we do not later accidentally reattach a returning client # to a sync-group that is no longer current. $prefs->client($_)->remove('syncgroupid'); - + # Give player time to disconnect Slim::Utils::Timers::setTimer($_, time() + 3, sub { shift->execute([ 'client', 'forget' ]); } @@ -357,7 +355,7 @@ sub clientConnectCommand { } } } - + $request->setStatusDone(); } @@ -369,21 +367,27 @@ sub clientForgetCommand { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); - + # Bug 6508 # Can have a timing race with client reconnecting before this command get executed if ($client->connected()) { main::INFOLOG && $log->info($client->id . ': not forgetting as connected again'); return; } - + + # Persist playback state like we would do when turning off a player, that is, treat a vanishing + # player that's still playing the same way as a player that's turned off while still playing, so + # we can make it start playing again if it reappears and the user told us to resume playing + # when powering on. + $client->persistPlaybackStateForPowerOff(); + $client->controller()->playerInactive($client); $client->forgetClient(); - + $request->setStatusDone(); } @@ -396,7 +400,7 @@ sub debugCommand { $request->setStatusBadDispatch(); return; } - + # get the parameters my $category = $request->getParam('_debugflag'); my $newValue = $request->getParam('_newvalue'); @@ -406,7 +410,7 @@ sub debugCommand { $request->setStatusBadParams(); return; } - + my $ret = Slim::Utils::Log->setLogLevelForCategory($category, $newValue); if ($ret == 0) { @@ -471,7 +475,7 @@ sub disconnectCommand { $http->get( $server . 'jsonrpc.js', $postdata); } - + $request->setStatusDone(); } @@ -491,18 +495,18 @@ sub displayCommand { my $line2 = $request->getParam('_line2'); my $duration = $request->getParam('_duration'); my $p4 = $request->getParam('_p4'); - + if (!defined $line1) { $request->setStatusBadParams(); return; } - + Slim::Buttons::ScreenSaver::wakeup($client); $client->showBriefly({ 'line' => [ $line1, $line2 ], }, $duration, $p4); - + $request->setStatusDone(); } @@ -518,15 +522,15 @@ sub irCommand { # get the parameters my $client = $request->client(); my $irCodeBytes = $request->getParam('_ircode'); - my $irTime = $request->getParam('_time'); - + my $irTime = $request->getParam('_time'); + if (!defined $irCodeBytes || !defined $irTime ) { $request->setStatusBadParams(); return; } - + Slim::Hardware::IR::processIR($client, $irCodeBytes, $irTime); - + $request->setStatusDone(); } @@ -543,7 +547,7 @@ sub irenableCommand { # get our parameters my $client = $request->client(); my $newenable = $request->getParam('_newvalue'); - + # handle toggle if (!defined $newenable) { @@ -557,7 +561,7 @@ sub irenableCommand { sub loggingCommand { my $request = shift; - + # check this is the correct command. if ($request->isNotCommand([['logging']])) { $request->setStatusBadDispatch(); @@ -565,7 +569,7 @@ sub loggingCommand { } my $group = uc( $request->getParam('group') ); - + if ($group && Slim::Utils::Log->logLevels($group)) { Slim::Utils::Log->setLogGroup($group, $request->getParam('persist')); } @@ -573,7 +577,7 @@ sub loggingCommand { $request->setStatusBadParams(); return; } - + $request->setStatusDone(); } @@ -595,7 +599,7 @@ sub mixerCommand { if (defined $sequenceNumber) { $client->sequenceNumber($sequenceNumber); } - + my $controllerSequenceId = $request->getParam('controllerSequenceId'); if (defined $controllerSequenceId) { $client->controllerSequenceId($controllerSequenceId); @@ -608,19 +612,19 @@ sub mixerCommand { if ($client->isSynced()) { @buddies = $client->syncedWith(); } - + if ($entity eq 'muting') { - + my $curmute = $prefs->client($client)->get('mute'); if ( !defined $newvalue || $newvalue eq 'toggle' ) { # toggle $newvalue = !$curmute; } - + if ($newvalue != $curmute) { my $vol = $client->volume(); my $fade; - + if ($newvalue == 0) { # need to un-mute volume @@ -637,14 +641,18 @@ sub mixerCommand { $prefs->client($client)->set('mute', 1); $fade = -0.3125; } - + $client->fade_volume($fade, \&_mixer_mute, [$client]); - - for my $eachclient (@buddies) { - if ($prefs->client($eachclient)->get('syncVolume')) { + # Bug 18165: do not sync volume if client's volume itself is not synced + if ($prefs->client($client)->get('syncVolume')) { + + for my $eachclient (@buddies) { + + if ($prefs->client($eachclient)->get('syncVolume')) { - $eachclient->fade_volume($fade, \&_mixer_mute, [$eachclient]); + $eachclient->fade_volume($fade, \&_mixer_mute, [$eachclient]); + } } } } @@ -675,15 +683,18 @@ sub mixerCommand { $newval = $client->$entity($newval); } - for my $eachclient (@buddies) { - if ($prefs->client($eachclient)->get('syncVolume')) { - $prefs->client($eachclient)->set($entity, $newval); - $eachclient->$entity($newval); - $eachclient->mixerDisplay('volume') if $entity eq 'volume'; + # Bug 18165: do not sync volume if client's volume itself is not synced + if ($prefs->client($client)->get('syncVolume')) { + for my $eachclient (@buddies) { + if ($prefs->client($eachclient)->get('syncVolume')) { + $prefs->client($eachclient)->set($entity, $newval); + $eachclient->$entity($newval); + $eachclient->mixerDisplay('volume') if $entity eq 'volume'; + } } } } - + if (defined $controllerSequenceId) { $client->controllerSequenceId(undef); $client->controllerSequenceNumber(undef); @@ -700,7 +711,7 @@ sub nameCommand { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); my $newValue = $request->getParam('_newvalue'); @@ -708,7 +719,7 @@ sub nameCommand { if (!defined $newValue || !defined $client) { $request->setStatusBadParams(); return; - } + } if ($newValue ne "0") { main::DEBUGLOG && $log->debug("PLAYERNAMECHANGE: " . $newValue); @@ -740,24 +751,24 @@ sub playcontrolCommand { my $newvalue = $request->getParam('_newvalue'); my $fadeIn = $request->getParam('_fadein'); my $suppressShowBriefly = $request->getParam('_suppressShowBriefly'); - + # which state are we in? my $curmode = Slim::Player::Source::playmode($client); - + # which state do we want to go to? my $wantmode = $cmd; - + # the "mode" command is deprecated, please do not use or fix if ($cmd eq 'mode') { - + # we want to go to $param if the command is mode $wantmode = $param; # and for pause we want 1 $newvalue = 1; } - + if ($cmd eq 'pause') { - + # pause 1, pause 0 and pause (toggle) are all supported, figure out which # one we want... if (defined $newvalue) { @@ -775,25 +786,25 @@ sub playcontrolCommand { # default to doing nothing... $wantmode = $curmode; - + # pause only from play $wantmode = 'pause' if $curmode eq 'play'; - } - + } + # do we need to do anything? if ($curmode ne $wantmode) { - + if ( $wantmode eq 'play' ) { # Bug 6813, 'play' from CLI needs to work the same as IR play button, by going # through playlist jump - this will include a showBriefly to give feedback my $index = Slim::Player::Source::playingSongIndex($client); - + $client->execute(['playlist', 'jump', $index, $fadeIn]); } else { # set new playmode Slim::Player::Source::playmode($client, $wantmode, undef, undef, $fadeIn); - + # give user feedback of new mode and current song if ($client->isPlayer()) { my $parts = $client->currentSongLines({ suppressDisplay => Slim::Buttons::Common::suppressStatus($client) }); @@ -804,7 +815,7 @@ sub playcontrolCommand { } } } - + $request->setStatusDone(); } @@ -825,13 +836,13 @@ sub playlistClearCommand { # called by currentPlaylistUpdateTime below # $client->currentPlaylistChangeTime(Time::HiRes::time()); - + $client->currentPlaylistUpdateTime(Time::HiRes::time()); - + # The above changes the playlist but I am not sure this is ever # executed, or even if it should be Slim::Player::Playlist::refreshPlaylist($client) if $client->currentPlaylistModified(); - + $request->setStatusDone(); } @@ -848,7 +859,7 @@ sub playlistDeleteCommand { # get the parameters my $client = $request->client(); my $index = $request->getParam('_index');; - + if (!defined $index) { $request->setStatusBadParams(); return; @@ -860,7 +871,7 @@ sub playlistDeleteCommand { #$client->currentPlaylistChangeTime(Time::HiRes::time()); $client->currentPlaylistUpdateTime(Time::HiRes::time()); Slim::Player::Playlist::refreshPlaylist($client); - + $request->setStatusDone(); } @@ -877,7 +888,7 @@ sub playlistDeleteitemCommand { # get the parameters my $client = $request->client(); my $item = $request->getParam('_item');; - + if (!$item) { $request->setStatusBadParams(); return; @@ -894,18 +905,18 @@ sub playlistDeleteitemCommand { } elsif (Slim::Music::Info::isDir($absitem)) { require Slim::Utils::Scanner; - + Slim::Utils::Scanner->scanPathOrURL({ 'url' => Slim::Utils::Misc::pathFromFileURL($absitem), 'listRef' => \@{$contents}, 'client' => $client, 'callback' => sub { my $foundItems = shift; - + Slim::Player::Playlist::removeMultipleTracks($client, $foundItems); }, }); - + } else { my $playlist = Slim::Schema->objectForUrl({ 'url' => $item, playlist => 1 }); @@ -941,7 +952,7 @@ sub playlistDeleteitemCommand { #$client->currentPlaylistChangeTime(Time::HiRes::time()); $client->currentPlaylistUpdateTime(Time::HiRes::time()); Slim::Player::Playlist::refreshPlaylist($client); - + $request->setStatusDone(); } @@ -961,12 +972,12 @@ sub playlistJumpCommand { my $fadeIn = $request->getParam('_fadein'); my $noplay = $request->getParam('_noplay'); my $seekdata = $request->getParam('_seekdata'); - + my $songcount = Slim::Player::Playlist::count($client) || return; - + my $newIndex = 0; my $isStopped = $client->isStopped(); - + if (!$client->power()) { $client->execute([ 'power', 1, 1 ]); } @@ -978,7 +989,7 @@ sub playlistJumpCommand { suppressDisplay => Slim::Buttons::Common::suppressStatus($client), jiveIconStyle => $jiveIconStyle, }); - + # awy: We used to set $parts->{'jive'}->{'duration'} = 10000 here in order to # ensure that the new track title is pushed to the first line of the display # and stays there until a new playerStatus arrives. This can take quite a @@ -988,7 +999,7 @@ sub playlistJumpCommand { # Having the popup hang around for 10s can be irritating (bug 17758). Given that # the player has control of title change itself, it can set a long duration # on just that element and use the default show-briefly duration for the popup. - + $client->showBriefly($parts, { duration => 2 }) if $parts; Slim::Buttons::Common::syncPeriodicUpdates($client, Time::HiRes::time() + 0.1); } @@ -996,71 +1007,71 @@ sub playlistJumpCommand { # Is this a relative jump, etc. if ( defined $index && $index =~ /[+-]/ ) { - + if (!$isStopped) { my $handler = $client->playingSong()->currentTrackHandler(); - + if ( ($songcount == 1 && $index eq '-1') || $index eq '+0' ) { # User is trying to restart the current track $client->controller()->jumpToTime(0, 1); $showStatus->('rew'); $request->setStatusDone(); - return; + return; } elsif ($index eq '+1') { # User is trying to skip to the next track $client->controller()->skip(); $showStatus->('fwd'); $request->setStatusDone(); - return; + return; } - + } - + $newIndex = Slim::Player::Source::playingSongIndex($client) + $index; main::INFOLOG && $log->info("Jumping by $index"); - + # Handle skip in repeat mode if ( $newIndex >= $songcount ) { # play the next song and start over if necessary - if (Slim::Player::Playlist::shuffle($client) && + if (Slim::Player::Playlist::shuffle($client) && Slim::Player::Playlist::repeat($client) == 2 && $prefs->get('reshuffleOnRepeat')) { Slim::Player::Playlist::reshuffle($client, 1); } } - + } else { $newIndex = $index if defined $index; main::INFOLOG && $log->info("Jumping to $newIndex"); } - + # Check for wrap-around if ($newIndex >= $songcount) { $newIndex %= $songcount; } elsif ($newIndex < 0) { $newIndex = ($newIndex + $songcount) % $songcount; } - + if ($noplay && $isStopped) { $client->controller()->resetSongqueue($newIndex); } else { main::INFOLOG && $log->info("playing $newIndex"); $client->controller()->play($newIndex, $seekdata, undef, $fadeIn); - } + } # Does the above change the playlist? Slim::Player::Playlist::refreshPlaylist($client) if $client->currentPlaylistModified(); # if we're jumping +1/-1 in the index let squeezeplay know this showBriefly is to be styled accordingly my $jiveIconStyle = undef; - if ($index eq '-1') { + if ($index && $index eq '-1') { $jiveIconStyle = 'rew'; - } elsif ($index eq '+1') { + } elsif ($index && $index eq '+1') { $jiveIconStyle = 'fwd'; } $showStatus->($jiveIconStyle); - + $request->setStatusDone(); } @@ -1077,7 +1088,7 @@ sub playlistMoveCommand { my $client = $request->client(); my $fromindex = $request->getParam('_fromindex');; my $toindex = $request->getParam('_toindex');; - + if (!defined $fromindex || !defined $toindex) { $request->setStatusBadParams(); return; @@ -1087,9 +1098,9 @@ sub playlistMoveCommand { $client->currentPlaylistModified(1); #$client->currentPlaylistChangeTime(Time::HiRes::time()); $client->currentPlaylistUpdateTime(Time::HiRes::time()); - + Slim::Player::Playlist::refreshPlaylist($client); - + $request->setStatusDone(); } @@ -1106,14 +1117,14 @@ sub playlistRepeatCommand { # get the parameters my $client = $request->client(); my $newvalue = $request->getParam('_newvalue'); - + if (!defined $newvalue) { # original code: (Slim::Player::Playlist::repeat($client) + 1) % 3 $newvalue = (1,2,0)[Slim::Player::Playlist::repeat($client)]; } - + Slim::Player::Playlist::repeat($client, $newvalue); - + $request->setStatusDone(); } @@ -1126,13 +1137,13 @@ sub playlistSaveCommand { $request->setStatusBadDispatch(); return; } - + # can't do much without playlistdir! if (!Slim::Utils::Misc::getPlaylistDir()) { $request->setStatusBadConfig(); return; } - + if (Slim::Music::Import->stillScanning()) { $request->addResult('writeError', 1); $request->setStatusDone(); @@ -1147,7 +1158,6 @@ sub playlistSaveCommand { $title = Slim::Utils::Misc::cleanupFilename($title); - my $playlistObj = Slim::Schema->updateOrCreate({ 'url' => Slim::Utils::Misc::fileURLFromPath( @@ -1167,7 +1177,7 @@ sub playlistSaveCommand { for my $shuffleitem (@{Slim::Player::Playlist::shuffleList($client)}) { push @$annotatedList, Slim::Player::Playlist::song($client, $shuffleitem, 0, 0); } - + } else { $annotatedList = Slim::Player::Playlist::playList($client); @@ -1183,9 +1193,15 @@ sub playlistSaveCommand { if (!defined Slim::Player::Playlist::scheduleWriteOfPlaylist($client, $playlistObj)) { $request->addResult('writeError', 1); } - + $request->addResult('__playlist_id', $playlistObj->id); + if ($client && $playlistObj->title ne Slim::Utils::Strings::string('UNTITLED')) { + $client->currentPlaylist($playlistObj); + $client->currentPlaylistUpdateTime(Time::HiRes::time()); + $client->currentPlaylistModified(0); + } + if ( ! $silent ) { $client->showBriefly({ 'jive' => { @@ -1210,19 +1226,19 @@ sub playlistShuffleCommand { # get the parameters my $client = $request->client(); my $newvalue = $request->getParam('_newvalue'); - + if (!defined $newvalue) { $newvalue = (1,2,0)[Slim::Player::Playlist::shuffle($client)]; } - + Slim::Player::Playlist::shuffle($client, $newvalue); Slim::Player::Playlist::reshuffle($client); #$client->currentPlaylistChangeTime(Time::HiRes::time()); $client->currentPlaylistUpdateTime(Time::HiRes::time()); - + # Does the above change the playlist? Slim::Player::Playlist::refreshPlaylist($client) if $client->currentPlaylistModified(); - + $request->setStatusDone(); } @@ -1286,11 +1302,11 @@ sub playlistPreviewCommand { } my $client = $request->client(); - my $cmd = $request->getParam('cmd'); - my $url = $request->getParam('url'); + my $cmd = $request->getParam('cmd'); + my $url = $request->getParam('url'); my $title = $request->getParam('title') || ''; my $fadeIn = $request->getParam('fadein') ? $request->getParam('fadein') : undef; - + # if we have a cmd of 'stop', load the most recent playlist (which will clear the preview) if ($cmd eq 'stop') { # stop and clear the current tone @@ -1316,11 +1332,11 @@ sub playlistPreviewCommand { main::INFOLOG && $log->info("queuing up ", $title, " for preview"); $client->execute( ['playlist', 'play', $url, $title, $fadeIn ] ); - + } $request->setStatusDone(); - + } sub _getPreviewPlaylistName { @@ -1359,17 +1375,17 @@ sub playlistXitemCommand { my $jumpToIndex = $request->getParam('play_index'); # This should be undef (by default) - see bug 2085 my $results; - + # If we're playing a list of URLs (from XMLBrowser), only work on the first item my $list; if ( ref $item eq 'ARRAY' ) { - + # If in shuffle mode, we need to shuffle the list of items as # soon as we get it if ( Slim::Player::Playlist::shuffle($client) == 1 ) { Slim::Player::Playlist::fischer_yates_shuffle($item); } - + $list = $item; $item = shift @{$item}; } @@ -1383,7 +1399,7 @@ sub playlistXitemCommand { main::INFOLOG && $log->info("url: $url"); my $path = $url; - + # Set title if supplied if ( $title ) { Slim::Music::Info::setTitle( $url, $title ); @@ -1391,9 +1407,9 @@ sub playlistXitemCommand { } # check whether url is potentially for some sort of db entry, if so pass to playlistXtracksCommand - # But not for or local file:// URLs, and this may mean + # But not for or local file:// URLs, and this may mean # rescanning items already in the database but still allows playlist and other favorites to be played - + # hardcoding these protocols isn't the best way to do this. We should be using the protocol handler's explodePlaylist call. if ($path =~ /^db:|^itunesplaylist:|^musicipplaylist:/) { if (my @tracks = _playlistXtracksCommand_parseDbItem($client, $path)) { @@ -1443,12 +1459,12 @@ sub playlistXitemCommand { } main::INFOLOG && $log->info("path: $path"); - + # bug 14760 - just continue where we were if what we are about to play is the # same as the single thing we are already playing if ( $cmd =~ /^(play|load)$/ && Slim::Player::Playlist::count($client) == 1 - && $client->playingSong() + && $client->playingSong() && $path eq $client->playingSong()->track()->url() && !$noplay ) { @@ -1459,7 +1475,7 @@ sub playlistXitemCommand { } elsif ( !$client->isPlaying('really') ) { Slim::Player::Source::playmode($client, 'play', undef, undef, $fadeIn); } - + # XXX: this should not be calling a request callback directly! # It should be handled by $request->setStatusDone if ( my $callbackf = $request->callbackFunction ) { @@ -1470,11 +1486,11 @@ sub playlistXitemCommand { $callbackf->( $request ); } } - + playlistXitemCommand_done($client, $request, $path); - + main::DEBUGLOG && $log->debug("done."); - + return; } @@ -1508,18 +1524,18 @@ sub playlistXitemCommand { # is this a track referenced from a cue sheet? my $isReferenced; - + if (!Slim::Music::Info::isRemoteURL( $fixedPath ) && Slim::Music::Info::isFileURL( $fixedPath ) ) { $path = Slim::Utils::Misc::pathFromFileURL($fixedPath); - + # referenced tracks come with a #start-end postfix in the url $isReferenced = ($fixedPath =~ /#\d+.*\d+$/ && $path !~ /#\d+.*\d+$/) ? 1 : 0; main::INFOLOG && $log->info("path: $path"); } - if ($cmd =~ /^(play|load)$/) { + if ($cmd =~ /^(play|load)$/) { $jumpToIndex = 0 if !defined $jumpToIndex; @@ -1569,7 +1585,7 @@ sub playlistXitemCommand { } } else { - + # Display some feedback for the player on remote URLs # XXX - why only remote URLs? if ( $cmd eq 'add' && Slim::Music::Info::isRemoteURL($path) && !Slim::Music::Info::isDigitalInput($path) && !Slim::Music::Info::isLineIn($path) ) { @@ -1582,7 +1598,7 @@ sub playlistXitemCommand { ], jive => { type => 'popupplay', - text => [ + text => [ $client->string('JIVE_POPUP_ADDING_TO_PLAYLIST', $insert) ], }, @@ -1590,7 +1606,7 @@ sub playlistXitemCommand { } require Slim::Utils::Scanner; - + Slim::Utils::Scanner->scanPathOrURL({ 'url' => $isReferenced ? $url : $path, 'listRef' => Slim::Player::Playlist::playList($client), @@ -1598,7 +1614,7 @@ sub playlistXitemCommand { 'cmd' => $cmd, 'callback' => sub { my ( $foundItems, $error ) = @_; - + # tracks referenced from cue sheet would not be found due to their special content type - add the raw URL back in if ( ref $foundItems eq 'ARRAY' && !scalar @$foundItems && $isReferenced ) { push @$foundItems, $url; @@ -1608,7 +1624,7 @@ sub playlistXitemCommand { my $noShuffle = 0; if ( ref $list eq 'ARRAY' ) { push @{$foundItems}, @{$list}; - + # XXX - we DO need to shuffle, as otherwise the client's shufflelist will not know about the newly added items! #$noShuffle = 1; } @@ -1700,7 +1716,7 @@ sub playlistXtracksCommand { if ($what =~ /urllist/i) { @tracks = _playlistXtracksCommand_constructTrackList($client, $what, $listref); - + } elsif ($what =~ /listRef/i) { @tracks = _playlistXtracksCommand_parseListRef($client, $what, $listref); @@ -1759,12 +1775,12 @@ sub playlistXtracksCommand { } } } - + $client->execute(['playlist', 'jump', $jumpToIndex, $fadeIn]); - + # Reshuffle (again) to get playing song or album at start of list Slim::Player::Playlist::reshuffle($client) if $load && defined $jumpToIndex && Slim::Player::Playlist::shuffle($client); - + $client->currentPlaylistModified(0); } @@ -1791,7 +1807,7 @@ sub playlistZapCommand { # get the parameters my $client = $request->client(); my $index = $request->getParam('_index');; - + my $zapped = $client->string('ZAPPED_SONGS'); my $zapindex = defined $index ? $index : Slim::Player::Source::playingSongIndex($client); my $zapsong = Slim::Player::Playlist::song($client, $zapindex); @@ -1823,14 +1839,14 @@ sub playlistZapCommand { $client->currentPlaylistModified(1); $client->currentPlaylistUpdateTime(Time::HiRes::time()); Slim::Player::Playlist::refreshPlaylist($client); - + $request->setStatusDone(); } sub playlistcontrolCommand { my $request = shift; - + main::INFOLOG && $log->info("Begin Function"); # check this is the correct command. @@ -1843,7 +1859,7 @@ sub playlistcontrolCommand { my $client = $request->client(); my $cmd = $request->getParam('cmd'); my $jumpIndex = $request->getParam('play_index'); - + if (Slim::Music::Import->stillScanning()) { $request->addResult('rescan', "1"); } @@ -1857,20 +1873,20 @@ sub playlistcontrolCommand { my $insert = ($cmd eq 'insert'); my $add = ($cmd eq 'add'); my $delete = ($cmd eq 'delete'); - + # shortcut to playlist $cmd url if given a folder_id... # the acrobatics it does are too risky to replicate if (defined(my $folderId = $request->getParam('folder_id'))) { - + # unfortunately playlist delete is not supported if ($delete) { $request->setStatusBadParams(); return; } - + my $folder = Slim::Schema->find($folderId < 0 ? 'RemoteTrack' : 'Track', $folderId); - + # make sure it's a folder if (!blessed($folder) || !$folder->can('url') || !$folder->can('content_type') || $folder->content_type() ne 'dir') { $request->setStatusBadParams(); @@ -1885,13 +1901,13 @@ sub playlistcontrolCommand { $token = 'JIVE_POPUP_ADDING_TO_PLAY_NEXT'; } my $string = $client->string($token, $folder->title); - $client->showBriefly({ - 'jive' => { + $client->showBriefly({ + 'jive' => { 'type' => 'popupplay', 'text' => [ $string ], } }); - } + } Slim::Control::Request::executeRequest( $client, ['playlist', $cmd, $folder->url(), ($load && $jumpIndex ? 'play_index:' . $jumpIndex : undef) ] @@ -1946,12 +1962,12 @@ sub playlistcontrolCommand { $token = 'JIVE_POPUP_NOW_PLAYING'; } my $string = $client->string($token, $playlist->title); - $client->showBriefly({ - 'jive' => { + $client->showBriefly({ + 'jive' => { 'type' => 'popupplay', 'text' => [ $string ], } - }); + }); } $cmd .= "tracks"; @@ -1959,7 +1975,7 @@ sub playlistcontrolCommand { my $library_id = Slim::Music::VirtualLibraries->getRealId($request->getParam('library_id')) || Slim::Music::VirtualLibraries->getLibraryIdForClient($client); my $query = 'playlist.id=' . $playlist_id; $query .= '&library_id=' . $library_id if $library_id; - + Slim::Control::Request::executeRequest( $client, ['playlist', $cmd, $query, undef, undef, $jumpIndex] ); @@ -1967,7 +1983,7 @@ sub playlistcontrolCommand { $request->addResult( 'count', $playlist->tracks($library_id)->count() ); $request->setStatusDone(); - + return; } @@ -1975,7 +1991,7 @@ sub playlistcontrolCommand { # split on commas my @track_ids = split(/,/, $track_id_list); - + # keep the order my %track_ids_order; my $i = 0; @@ -1985,7 +2001,7 @@ sub playlistcontrolCommand { # find the tracks my @rawtracks = Slim::Schema->search('Track', { 'id' => { 'in' => \@track_ids } })->all; - + # we might have remote tracks (negative ID) in the list foreach ( grep /-\d/, @track_ids ) { # We don't have a Slim::Schema::ResultSet::RemoteTrack... @@ -1993,7 +2009,7 @@ sub playlistcontrolCommand { push @rawtracks, $track; } } - + # sort them back! @tracks = sort { $track_ids_order{$a->id()} <=> $track_ids_order{$b->id()} } @rawtracks; @@ -2002,9 +2018,9 @@ sub playlistcontrolCommand { } else { # rather than re-invent the wheel, use _playlistXtracksCommand_parseSearchTerms - + my $what = {}; - + if (defined(my $genre_id = $request->getParam('genre_id'))) { $what->{'genre.id'} = { 'in' => [ split(/,/, $genre_id) ] }; $info[0] = join(', ', map { $_->name } Slim::Schema->search('Genre', { 'id' => { 'in' => [ split(/,/, $genre_id) ] } })->all); @@ -2036,7 +2052,11 @@ sub playlistcontrolCommand { $what->{'year.id'} = $year_id; } - + + if (defined(my $sort = $request->getParam('sort'))) { + $what->{'sort'} = $sort; + } + @tracks = _playlistXtracksCommand_parseSearchTerms($client, $what, $cmd); } @@ -2059,8 +2079,8 @@ sub playlistcontrolCommand { # not to be shown for now playing, as we're pushing to now playing screen now and no need for showBriefly if ($showBriefly) { my $string = $client->string($token); - $client->showBriefly({ - 'jive' => { + $client->showBriefly({ + 'jive' => { 'type' => 'mixed', 'style' => 'add', 'text' => [ $string, $info[0] ], @@ -2126,7 +2146,7 @@ sub playlistsEditCommand { $request->setStatusBadParams(); return; } - + # now perform the operation my @items = $playlist->tracks; my $changed = 0; @@ -2164,7 +2184,7 @@ sub playlistsEditCommand { } elsif ($cmd eq 'move') { - if ($itemPos != $newPos && $itemPos < scalar(@items) && $itemPos >= 0 + if ($itemPos != $newPos && $itemPos < scalar(@items) && $itemPos >= 0 && $newPos < scalar(@items) && $newPos >= 0) { # extract the item to be moved @@ -2174,7 +2194,7 @@ sub playlistsEditCommand { $changed = 1; } - + } elsif ($cmd eq 'add') { # Add function - Add entry it not already in list @@ -2231,7 +2251,7 @@ sub playlistsEditCommand { Slim::Formats::Playlists->writeList(\@items, undef, $playlist->url); } - + $playlist = undef; Slim::Schema->forceCommit; @@ -2271,13 +2291,21 @@ sub playlistsDeleteCommand { if ($request->source && $request->source =~ /\/slim\/request/) { $request->client->showBriefly({ 'jive' => { - 'text' => [ + 'text' => [ $request->string('JIVE_DELETE_PLAYLIST', $playlistObj->name) ], }, }); } + foreach my $client ( Slim::Player::Client::clients() ) { + if ($client->currentPlaylist && $client->currentPlaylist->id == $playlistObj->id) { + # must send defined, but falsy value + $client->currentPlaylist(0); + $client->currentPlaylistUpdateTime(Time::HiRes::time()); + } + } + _wipePlaylist($playlistObj); $request->setStatusDone(); @@ -2288,7 +2316,7 @@ sub _wipePlaylist { my $playlistObj = shift; - + if ( ! ( blessed($playlistObj) && $playlistObj->isPlaylist() ) ) { $log->error('PlaylistObj not right for this sub: ', $playlistObj); $log->error('PlaylistObj not right for this sub: ', $playlistObj->isPlaylist() ); @@ -2296,7 +2324,7 @@ sub _wipePlaylist { } Slim::Player::Playlist::removePlaylistFromDisk($playlistObj); - + # Do a fast delete, and then commit it. $playlistObj->setTracks([]); $playlistObj->delete; @@ -2316,7 +2344,7 @@ sub playlistsNewCommand { $request->setStatusBadDispatch(); return; } - + # can't do much without playlistdir! if (!Slim::Utils::Misc::getPlaylistDir()) { $request->setStatusBadConfig(); @@ -2366,7 +2394,7 @@ sub playlistsNewCommand { $request->addResult('playlist_id', $playlistObj->id); } - + $request->setStatusDone(); } @@ -2401,7 +2429,7 @@ sub playlistsRenameCommand { $newName = Slim::Utils::Misc::cleanupFilename($newName); # now perform the operation - + my $newUrl = Slim::Utils::Misc::fileURLFromPath( catfile(Slim::Utils::Misc::getPlaylistDir(), Slim::Utils::Unicode::encode_locale($newName) . '.m3u') ); @@ -2415,7 +2443,7 @@ sub playlistsRenameCommand { $request->addResult("overwritten_playlist_id", $existingPlaylist->id()); } - + if (!$dry_run) { if (blessed($existingPlaylist) && $existingPlaylist->id ne $playlistObj->id) { @@ -2424,7 +2452,7 @@ sub playlistsRenameCommand { $existingPlaylist = undef; } - + my $index = Slim::Formats::Playlists::M3U->readCurTrackForM3U( $playlistObj->path ); Slim::Player::Playlist::removePlaylistFromDisk($playlistObj); @@ -2434,9 +2462,31 @@ sub playlistsRenameCommand { $playlistObj->set_column('title', $newName); $playlistObj->set_column('titlesort', Slim::Utils::Text::ignoreCaseArticles($newName)); $playlistObj->set_column('titlesearch', Slim::Utils::Text::ignoreCase($newName, 1)); + $playlistObj->set_column('updated_time', time()); $playlistObj->update; - if (!defined Slim::Formats::Playlists::M3U->write( + # tell clients to pick up the new name + foreach my $client ( Slim::Player::Client::clients() ) { + if ($client->currentPlaylist && $client->currentPlaylist->id == $playlistObj->id) { + $client->currentPlaylist($playlistObj); + $client->currentPlaylistUpdateTime(Time::HiRes::time()); + } + } + + # check whether we're saving the current player's track queue as a playlist + my $client = $request->client; + if ($client && !$client->currentPlaylist) { + my $playlistTrackIds = join(':', map { $_->id } $playlistObj->tracks); + my $playerTrackIds = join(':', map { $_->id} @{$client->playlist}); + + if ($playlistTrackIds eq $playerTrackIds) { + $client->currentPlaylist($playlistObj); + $client->currentPlaylistUpdateTime(Time::HiRes::time()); + $client->currentPlaylistModified(0); + } + } + + if (!defined Slim::Formats::Playlists::M3U->write( [ $playlistObj->tracks ], undef, $playlistObj->path, @@ -2475,18 +2525,18 @@ sub powerCommand { } if ($newpower == $client->power()) {return;} - + # handle sync'd players if ($client->isSynced()) { my @buddies = $client->syncedWith(); - + for my $eachclient (@buddies) { $eachclient->power($newpower, 1) if $prefs->client($eachclient)->get('syncPower'); - + # send an update for Jive player power menu Slim::Control::Jive::playerPower($eachclient); - + } } @@ -2499,7 +2549,7 @@ sub powerCommand { $client->sleepTime(0); $client->currentSleepTime(0); } - + # send an update for Jive player power menu Slim::Control::Jive::playerPower($client); @@ -2518,10 +2568,10 @@ sub prefCommand { my $client; if ($request->isCommand([['playerpref']])) { - + $client = $request->client(); - - unless ($client) { + + unless ($client) { $request->setStatusBadDispatch(); return; } @@ -2537,7 +2587,7 @@ sub prefCommand { $namespace = $1; $prefName = $2; } - + if ($newValue =~ /^value:/) { $newValue =~ s/^value://; } @@ -2545,7 +2595,7 @@ sub prefCommand { if (!defined $prefName || !defined $newValue || !defined $namespace) { $request->setStatusBadParams(); return; - } + } if ($client) { preferences($namespace)->client($client)->set($prefName, $newValue); @@ -2553,7 +2603,7 @@ sub prefCommand { else { preferences($namespace)->set($prefName, $newValue); } - + $request->setStatusDone(); } @@ -2569,10 +2619,10 @@ sub rescanCommand { my $originalMode; my $mode = $originalMode = $request->getParam('_mode') || 'full'; my $singledir = $request->getParam('_singledir'); - + if ($singledir) { $singledir = Slim::Utils::Misc::pathFromFileURL($singledir); - + # don't run scan if newly added entry is disabled for all media types if ( grep { /\Q$singledir\E/ } @{ Slim::Utils::Misc::getInactiveMediaDirs() }) { main::INFOLOG && $log->info("Ignore scan request for folder, it's disabled for all media types: $singledir"); @@ -2582,25 +2632,27 @@ sub rescanCommand { } # if scan is running or we're told to queue up requests, return quickly + # FIXME - this seems to sometimes lead to infinite loops! (see eg. Synology change) if ( Slim::Music::Import->stillScanning() || Slim::Music::Import->doQueueScanTasks() || Slim::Music::Import->hasScanTask() ) { Slim::Music::Import->queueScanTask($request); - + # trigger the scan queue if we're not scanning yet Slim::Music::Import->nextScanTask() unless Slim::Music::Import->stillScanning() || Slim::Music::Import->doQueueScanTasks(); - + $request->setStatusDone(); return; } - + # Bug 17358, if any plugin importers are enabled such as iTunes/MusicIP, run an old-style external rescan # XXX Rewrite iTunes and MusicIP to support async rescan my $importers = Slim::Music::Import->importers(); while ( my ($class, $config) = each %{$importers} ) { if ( $class =~ /(?:Plugin|Slim::Music::VirtualLibraries)/ && $config->{use} ) { $mode = 'external'; + last; } } - + if ( $mode eq 'external' ) { # The old way of rescanning using scanner.pl my %args = (); @@ -2610,33 +2662,31 @@ sub rescanCommand { } else { $args{rescan} = 1; - } - + } + $args{singledir} = $singledir if $singledir; Slim::Music::Import->launchScan(\%args); } else { - # In-process scan - + # In-process scan + my @dirs = @{ Slim::Utils::Misc::getMediaDirs() }; # if we're scanning already, don't do it twice if (scalar @dirs) { - + if ( Slim::Utils::OSDetect::getOS->canAutoRescan && $prefs->get('autorescan') ) { require Slim::Utils::AutoRescan; Slim::Utils::AutoRescan->shutdown; } - + Slim::Utils::Progress->clear(); - + # we only want to scan folders for video/pictures my %seen = (); # to avoid duplicates - @dirs = grep { !$seen{$_}++ } @{ Slim::Utils::Misc::getVideoDirs() }, @{ Slim::Utils::Misc::getImageDirs() }; - - if ($singledir) { - @dirs = grep { /\Q$singledir\E/ } @dirs; - } + @dirs = grep { + !$seen{$_}++ + } @{ Slim::Utils::Misc::getVideoDirs($singledir) }, @{ Slim::Utils::Misc::getImageDirs($singledir) }; if ( main::MEDIASUPPORT && scalar @dirs && $mode ne 'playlists' ) { require Slim::Utils::Scanner::LMS; @@ -2655,34 +2705,26 @@ sub rescanCommand { } ); } }; - + # Audio scan is run first, when done, the LMS scanner is run - my $audio; - $audio = sub { - my $audiodirs = Slim::Utils::Misc::getAudioDirs(); - - if ($singledir) { - $audiodirs = [ grep { /\Q$singledir\E/ } @{$audiodirs} ]; - } - elsif (my $playlistdir = Slim::Utils::Misc::getPlaylistDir()) { - # scan playlist folder too - push @$audiodirs, $playlistdir; - } - - # XXX until libmediascan supports audio, run the audio scanner now - Slim::Utils::Scanner::Local->rescan( $audiodirs, { - types => 'list|audio', - scanName => 'directory', - progress => 1, - onFinished => $lms, - } ); - }; - - $audio->(); + my $audiodirs = Slim::Utils::Misc::getAudioDirs($singledir); + + if (my $playlistdir = Slim::Utils::Misc::getPlaylistDir()) { + # scan playlist folder too + push @$audiodirs, $playlistdir; + } + + # XXX until libmediascan supports audio, run the audio scanner now + Slim::Utils::Scanner::Local->rescan( $audiodirs, { + types => 'list|audio', + scanName => 'directory', + progress => 1, + onFinished => $lms, + } ); } elsif ($mode eq 'playlists') { my $playlistdir = Slim::Utils::Misc::getPlaylistDir(); - + # XXX until libmediascan supports audio, run the audio scanner now Slim::Utils::Scanner::Local->rescan( $playlistdir, { types => 'list', @@ -2691,16 +2733,13 @@ sub rescanCommand { } ); } else { - my $audiodirs = Slim::Utils::Misc::getAudioDirs(); - - if ($singledir) { - $audiodirs = [ grep { /\Q$singledir\E/ } @{$audiodirs} ]; - } - elsif (my $playlistdir = Slim::Utils::Misc::getPlaylistDir()) { + my $audiodirs = Slim::Utils::Misc::getAudioDirs($singledir); + + if (my $playlistdir = Slim::Utils::Misc::getPlaylistDir()) { # scan playlist folder too push @$audiodirs, $playlistdir; } - + # XXX until libmediascan supports audio, run the audio scanner now Slim::Utils::Scanner::Local->rescan( $audiodirs, { types => 'list|audio', @@ -2727,15 +2766,15 @@ sub setSNCredentialsCommand { if (!main::NOMYSB) { my $password = $request->getParam('_password'); my $sync = $request->getParam('sync'); my $client = $request->client; - + # Sync can be toggled without username/password if ( defined $sync ) { $prefs->set('sn_sync', $sync); - + if ( UNIVERSAL::can('Slim::Networking::SqueezeNetwork::PrefSync', 'shutdown') ) { Slim::Networking::SqueezeNetwork::PrefSync->shutdown(); } - + if ( $sync ) { require Slim::Networking::SqueezeNetwork::PrefSync; Slim::Networking::SqueezeNetwork::PrefSync->init(); @@ -2743,10 +2782,10 @@ sub setSNCredentialsCommand { if (!main::NOMYSB) { } $password = sha1_base64($password); - + # Verify username/password if ($username) { - + $request->setStatusProcessing(); Slim::Networking::SqueezeNetwork->login( @@ -2756,27 +2795,29 @@ sub setSNCredentialsCommand { if (!main::NOMYSB) { cb => sub { $request->addResult('validated', 1); $request->addResult('warning', $request->cstring('SETUP_SN_VALID_LOGIN')); - + # Shut down all SN activity Slim::Networking::SqueezeNetwork->shutdown(); - + $prefs->set('sn_email', $username); $prefs->set('sn_password_sha', $password); - + # Start it up again if the user enabled it Slim::Networking::SqueezeNetwork->init(); - + $request->setStatusDone(); }, ecb => sub { + my (undef, $error) = @_; $request->addResult('validated', 0); - $request->addResult('warning', $request->cstring('SETUP_SN_INVALID_LOGIN')); - + $request->addResult('warning', $request->cstring('SETUP_SN_INVALID_LOGIN') . ($error ? " ($error)" : '')); + $request->setStatusDone(); }, + interactive => 1, # tell login() to attempt without respecting local rate limiting ); } - + # stop SN integration if either mail or password is undefined else { $request->addResult('validated', 1); @@ -2804,7 +2845,7 @@ sub showCommand { my $font = $request->getParam('font'); my $centered = $request->getParam('centered'); my $screen = $request->getParam('screen'); - + if (!defined $line1 && !defined $line2) { $request->setStatusBadParams(); return; @@ -2814,21 +2855,21 @@ sub showCommand { $duration = 3 unless defined($duration); my $hash = {}; - + if ($centered) { $hash->{'center'} = [$line1, $line2]; } else { $hash->{'line'} = [$line1, $line2]; } - + if ($font eq 'huge') { $hash->{'fonts'} = { 'graphic-320x32' => 'full', 'graphic-160x32' => 'full_n', 'graphic-280x16' => 'huge', 'text' => 1, - }; + }; } else { $hash->{'fonts'} = { @@ -2850,8 +2891,8 @@ sub showCommand { } # call showBriefly for the magic! - $client->showBriefly($hash, - $duration, + $client->showBriefly($hash, + $duration, 0, # line2 is single line 1, # block updates 1, # scroll to end @@ -2876,7 +2917,7 @@ sub sleepCommand { # get our parameters my $client = $request->client(); my $will_sleep_in = $request->getParam('_newvalue'); - + if (!defined $will_sleep_in) { $request->setStatusBadParams(); return; @@ -2886,31 +2927,31 @@ sub sleepCommand { # Cancel the timers, we'll set them back if needed Slim::Utils::Timers::killTimers($client, \&_sleepStartFade); Slim::Utils::Timers::killTimers($client, \&_sleepPowerOff); - + # if we have a sleep duration if ($will_sleep_in > 0) { - + my $now = Time::HiRes::time(); - + # this is when we want to power off my $offTime = $now + $will_sleep_in; - + # this is the time we have to fade my $fadeDuration = $offTime - $now - 1; # fade for the last 60 seconds max - $fadeDuration = 60 if ($fadeDuration > 60); + $fadeDuration = 60 if ($fadeDuration > 60); # time at which we start fading my $fadeTime = $offTime - $fadeDuration; - + # set our timers Slim::Utils::Timers::setTimer($client, $offTime, \&_sleepPowerOff); - Slim::Utils::Timers::setTimer($client, $fadeTime, + Slim::Utils::Timers::setTimer($client, $fadeTime, \&_sleepStartFade, $fadeDuration); $client->sleepTime($offTime); # for some reason this is minutes... - $client->currentSleepTime($will_sleep_in / 60); + $client->currentSleepTime($will_sleep_in / 60); my $will_sleep_in_minutes = int( $will_sleep_in / 60 ); # only show a showBriefly if $will_sleep_in_minutes has a style on SP side @@ -2923,7 +2964,7 @@ sub sleepCommand { }; if ($validSleepStyles->{$will_sleep_in_minutes}) { $client->showBriefly({ - 'jive' => { + 'jive' => { 'type' => 'icon', 'style' => 'sleep_' . $will_sleep_in_minutes, 'text' => [ $will_sleep_in_minutes ], @@ -2939,7 +2980,7 @@ sub sleepCommand { }, }); } - + } else { # finish canceling any sleep in progress @@ -2947,13 +2988,13 @@ sub sleepCommand { $client->currentSleepTime(0); $client->showBriefly({ - 'jive' => { + 'jive' => { 'type' => 'icon', 'style' => 'sleep_cancel', 'text' => [ '' ], } }); - + } @@ -2976,7 +3017,7 @@ sub stopServer { else { main::stopServer(); } - + $request->setStatusDone(); } @@ -2994,16 +3035,16 @@ sub syncCommand { my $client = $request->client(); my $newbuddy = $request->getParam('_indexid-'); my $noRestart= $request->getParam('noRestart'); - + if (!defined $newbuddy) { $request->setStatusBadParams(); return; } - + if ($newbuddy eq '-') { - + $client->controller()->unsync($client); - + } else { # try an ID @@ -3016,10 +3057,10 @@ sub syncCommand { $buddy = $clients[$newbuddy]; } } - + $client->controller()->sync($buddy, $noRestart) if defined $buddy; } - + $request->setStatusDone(); } @@ -3037,14 +3078,14 @@ sub timeCommand { # get our parameters my $client = $request->client(); my $newtime = $request->getParam('_newvalue'); - + if (!defined $newtime) { $request->setStatusBadParams(); return; } - + Slim::Player::Source::gototime($client, $newtime); - + $request->setStatusDone(); } @@ -3061,7 +3102,7 @@ sub wipecacheCommand { if ( Slim::Music::Import->stillScanning() || Slim::Music::Import->doQueueScanTasks || $request->getParam('_queue') ) { Slim::Music::Import->queueScanTask($request); } - + else { # replace local tracks with volatile versions - we're gong to wipe the database @@ -3069,35 +3110,18 @@ sub wipecacheCommand { Slim::Player::Playlist::makeVolatile($client); } - + if ( Slim::Utils::OSDetect::getOS->canAutoRescan && $prefs->get('autorescan') ) { require Slim::Utils::AutoRescan; Slim::Utils::AutoRescan->shutdown; } Slim::Utils::Progress->clear(); - - if ( Slim::Utils::OSDetect::isSqueezeOS() ) { - # Wipe/rescan in-process on SqueezeOS - - # XXX - for the time being we're going to assume that the embedded server will only handle one folder - my $dir = Slim::Utils::Misc::getAudioDirs()->[0]; - - my %args = ( - types => 'list|audio', - scanName => 'directory', - progress => 1, - wipe => 1, - ); - - Slim::Utils::Scanner::Local->rescan( $dir, \%args ); - } - else { - # Launch external scanner on normal systems - Slim::Music::Import->launchScan( { - wipe => 1, - } ); - } + + # Launch external scanner on normal systems + Slim::Music::Import->launchScan( { + wipe => 1, + } ); } $request->setStatusDone(); @@ -3156,7 +3180,7 @@ sub ratingCommand { } else { $rating = Slim::Schema->rating($track); - + $request->addResult( '_rating', defined $rating ? $rating : 0 ); } @@ -3165,12 +3189,12 @@ sub ratingCommand { sub pragmaCommand { my $request = shift; - + my $pragma = join( ' ', grep { $_ ne 'pragma' } $request->renderAsArray ); - + # XXX need to pass pragma to artwork cache even if using MySQL Slim::Utils::OSDetect->getOS()->sqlHelperClass()->pragma($pragma); - + $request->setStatusDone(); } @@ -3181,7 +3205,7 @@ sub pragmaCommand { sub _sleepStartFade { my $client = shift; my $fadeDuration = shift; - + if ($client->isPlayer()) { $client->fade_volume(-$fadeDuration); } @@ -3192,7 +3216,7 @@ sub _sleepPowerOff { $client->sleepTime(0); $client->currentSleepTime(0); - + Slim::Control::Request::executeRequest($client, ['stop']); Slim::Control::Request::executeRequest($client, ['power', 0]); } @@ -3206,7 +3230,7 @@ sub _mixer_mute { sub _playlistXitem_load_done { my ($client, $index, $request, $count, $url, $error, $noShuffle, $fadeIn, $noplay, $wipePlaylist) = @_; - + # dont' keep current song on loading a playlist if ( !$noShuffle ) { Slim::Player::Playlist::reshuffle($client, @@ -3282,16 +3306,14 @@ sub _playlistXtracksCommand_parseSearchTerms { # Bug: 3629 - sort by album, then disc, tracknum, titlesort my $sqlHelperClass = Slim::Utils::OSDetect->getOS()->sqlHelperClass(); - + my $collate = $sqlHelperClass->collate(); - - my $albumSort - = $sqlHelperClass->append0("album.titlesort") . " $collate" - . ', me.disc, me.tracknum, ' - . $sqlHelperClass->append0("me.titlesort") . " $collate"; - + + my $albumSort = $sqlHelperClass->append0("album.titlesort") . " $collate" + . ', me.disc, me.tracknum, ' . $sqlHelperClass->append0("me.titlesort") . " $collate"; + my $albumYearSort = $sqlHelperClass->append0("album.year") . ", " . $albumSort; my $trackSort = "me.disc, me.tracknum, " . $sqlHelperClass->append0("me.titlesort") . " $collate"; - + if ( !Slim::Schema::hasLibrary()) { return (); } @@ -3338,7 +3360,7 @@ sub _playlistXtracksCommand_parseSearchTerms { elsif ($key eq 'contributor.role') { next; } - + elsif (lc($key) eq 'librarytracks.library') { $library_id = $value; next; @@ -3395,14 +3417,10 @@ sub _playlistXtracksCommand_parseSearchTerms { $attrs{$key} = $value; - } elsif ($key eq 'sort') { - - $sort = $value; - - } else { + } elsif ($key ne 'sort') { # 'sort' handled afterwards... if ($key =~ /\.(?:name|title)search$/) { - + # BUG 4536: if existing value is a hash, don't create another one if (ref $value eq 'HASH') { $find{$key} = $value; @@ -3417,7 +3435,22 @@ sub _playlistXtracksCommand_parseSearchTerms { } } - # + # Check for 'sort' after iterating other keys, so that it takes precedence... + if (defined($terms->{'sort'})) { + my $value = $terms->{'sort'}; + + if ($value eq 'artflow' || $value eq 'yearartistalbum' || $value eq 'yearalbum') { + # If its an album sort where year takes precedence, then sort by year first + if ($sort eq $albumSort) { + $sort = $albumYearSort; + } + } elsif ($value !~ /^(artistalbum|albumtrack|new|random)$/) { + # Only use sort value if it is **not** an album sort. + $sort = $value; + } + } + + # if ($find{'playlist.id'} && !$find{'me.id'}) { # Treat playlists specially - they are containers. @@ -3436,7 +3469,7 @@ sub _playlistXtracksCommand_parseSearchTerms { # on search, only grab audio items. $find{'audio'} = 1; - + my $vaObjId = Slim::Schema->variousArtistsObject->id; if ($find{'contributor.id'} && $find{'contributor.id'} == $vaObjId) { @@ -3451,14 +3484,14 @@ sub _playlistXtracksCommand_parseSearchTerms { delete $find{'contributor.id'}; } - if ($find{'me.album'} && $find{'contributor.id'} && + if ($find{'me.album'} && $find{'contributor.id'} && $find{'contributor.id'} == $vaObjId) { delete $find{'contributor.id'}; } if ($find{'playlist.id'}) { - + delete $find{'playlist.id'}; } @@ -3474,8 +3507,8 @@ sub _playlistXtracksCommand_parseSearchTerms { $find{'album.year'} = delete $find{'year.id'}; delete $joinMap{'year'}; } - - if ($sort && $sort eq $albumSort) { + + if ($sort && ($sort eq $albumSort || $sort eq $albumYearSort)) { if ($find{'me.album'}) { # Don't need album-sort if we have a specific album-id $sort = undef; @@ -3504,7 +3537,7 @@ sub _playlistXtracksCommand_constructTrackList { my $client = shift; my $term = shift; my $list = shift; - + my @list = split (/,/, $list); my @tracks = (); for my $url ( @list ) { @@ -3565,13 +3598,13 @@ sub _playlistXtracksCommand_parseDbItem { # Bug: 2569 # We need to ask for the right type of object. - # + # # Contributors, Genres & Albums have a url of: # db:contributor.namesearch=BEATLES # # Remote playlists are Track objects, not Playlist objects. if ($url =~ /^db:(\w+\.\w+=.+)$/) { - + for my $term ( split '&', $1 ) { # If $terms has a leading &, split will generate an initial empty string @@ -3590,14 +3623,14 @@ sub _playlistXtracksCommand_parseDbItem { } $class = ucfirst($1); - + if ( $class eq 'LibraryTracks' && $key eq 'library' && $value eq '-1' ) { $obj = -1; } else { $obj = Slim::Schema->single( $class, { $key => $value } ); } - + $classes{$class} = $obj; } } @@ -3626,22 +3659,22 @@ sub _playlistXtracksCommand_parseDbItem { my $terms = ""; while ( ($class, $obj) = each %classes ) { if ( blessed($obj) && ( - $class eq 'Album' || - $class eq 'Contributor' || + $class eq 'Album' || + $class eq 'Contributor' || $class eq 'Genre' || $class eq 'Year' || - ( blessed $obj && $obj->can('content_type') && $obj->content_type ne 'dir') + ( blessed $obj && $obj->can('content_type') && $obj->content_type ne 'dir') ) ) { $terms .= "&" if ( $terms ne "" ); $terms .= sprintf( '%s.id=%d', lc($class), $obj->id ); } } - + if ( $classes{LibraryTracks} ) { $terms .= "&" if ( $terms ne "" ); $terms .= sprintf( 'librarytracks.library=%d', $classes{LibraryTracks} ); } - + if ( $terms ne "" ) { return _playlistXtracksCommand_parseSearchTerms($client, $terms); } @@ -3655,7 +3688,7 @@ sub _showCommand_done { my $request = $args->{'request'}; my $client = $request->client(); - + # now we're done! $request->setStatusDone(); } diff --git a/Slim/Control/Jive.pm b/Slim/Control/Jive.pm index fc3d01eb815..e5ed5a3b15a 100644 --- a/Slim/Control/Jive.pm +++ b/Slim/Control/Jive.pm @@ -1,8 +1,8 @@ package Slim::Control::Jive; -# Logitech Media Server Copyright 2001-2011 Logitech +# Logitech Media Server Copyright 2001-2020 Logitech # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -46,7 +46,7 @@ sub init { my $class = shift; # register our functions - + # |requires Client (2 == set disconnected client to clientid if client does not exist) # | |is a Query # | | |has Tags @@ -145,19 +145,19 @@ sub init { Slim::Control::Request::addDispatch(['jivepatches'], [0, 1, 1, \&extensionsQuery]); - + # setup the menustatus dispatch and subscription Slim::Control::Request::addDispatch( ['menustatus', '_data', '_action'], [0, 0, 0, sub { warn "menustatus query\n" }]); - + if ( $log->is_info ) { Slim::Control::Request::subscribe( \&menuNotification, [['menustatus']] ); } - + # setup a cli command for jive that returns nothing; can be useful in some situations Slim::Control::Request::addDispatch( ['jiveblankcommand'], [0, 0, 0, sub { return 1; }]); - + } # keep this around for backwards compatibility - should not be needed... @@ -195,48 +195,48 @@ sub menuQuery { my $client = $request->client() || 0; my $disconnected; - + if ( !$client ) { require Slim::Player::Disconnected; - + # Check if this is a disconnected player request if ( my $id = $request->disconnectedClientID ) { - + $client = Slim::Player::Disconnected->new($id); $disconnected = 1; - + main::INFOLOG && $log->is_info && $log->info("Player $id not connected, using disconnected menu mode"); } else { # XXX temporary workaround for requests without a playerid $client = Slim::Player::Disconnected->new( '_dummy_' . Time::HiRes::time() ); $disconnected = 1; - + $log->error("Menu requests without a client are deprecated, using disconnected menu mode"); } } - + my $direct = ( $disconnected || $request->getParam('direct') ) ? 1 : 0; # send main menu notification my $menu = mainMenu($client, $direct); - + # Return results directly and destroy the client if it is disconnected # Also return the results directly if param 'direct' is set if ( $direct ) { $log->is_info && $log->info('Sending direct menu response'); - + $request->setRawResults( { count => scalar @{$menu}, offset => 0, item_loop => $menu, } ); - + if ( $disconnected ) { $client->forgetClient; } } - + $request->setStatusDone(); } @@ -245,12 +245,12 @@ sub mainMenu { main::INFOLOG && $log->info("Begin function"); my $client = shift; my $direct = shift; - + unless ($client && $client->isa('Slim::Player::Client')) { # if this isn't a player, no menus should get sent return; } - + # as a convention, make weights => 10 and <= 100; Jive items that want to be below all SS items # then just need to have a weight > 100, above SS items < 10 @@ -321,7 +321,7 @@ sub mainMenu { if ( !$direct ) { _notifyJive(\@menu, $client); } - + return \@menu; } @@ -364,7 +364,7 @@ sub albumSortSettingsMenu { $request->addResultLoop('item_loop', $i, 'actions', $actions); $i++; } - + } sub albumSortSettingsItem { @@ -423,10 +423,10 @@ sub registerPluginNode { sub registerAppMenu { my $menuArray = shift; - + # now we want all of the items in $menuArray to go into @pluginMenus, but we also - # don't want duplicate items (specified by 'id'), - # so we want the ids from $menuArray to stomp on ids from @pluginMenus, + # don't want duplicate items (specified by 'id'), + # so we want the ids from $menuArray to stomp on ids from @pluginMenus, # thus getting the "newest" ids into the @pluginMenus array of items # we also do not allow any hash without an id into the array, and will log an error if that happens @@ -451,7 +451,7 @@ sub registerAppMenu { } # @new is the new @appMenus - # we do this in reverse so we get previously initialized nodes first + # we do this in reverse so we get previously initialized nodes first # you can't add an item to a node that doesn't exist :) @appMenus = reverse @new; } @@ -473,7 +473,7 @@ sub registerPluginMenu { $log->error("Incorrect data type"); return; } - + my $isInfo = $log->is_info; if ($node) { @@ -493,8 +493,8 @@ sub registerPluginMenu { } # now we want all of the items in $menuArray to go into @pluginMenus, but we also - # don't want duplicate items (specified by 'id'), - # so we want the ids from $menuArray to stomp on ids from @pluginMenus, + # don't want duplicate items (specified by 'id'), + # so we want the ids from $menuArray to stomp on ids from @pluginMenus, # thus getting the "newest" ids into the @pluginMenus array of items # we also do not allow any hash without an id into the array, and will log an error if that happens @@ -503,13 +503,13 @@ sub registerPluginMenu { for my $href (@$menuArray, reverse @pluginMenus) { my $id = $href->{'id'}; my $node = $href->{'node'}; - + # allow plugins to add themselves to the My Apps menu if ($href->{node} && $href->{node} eq 'apps') { $href->{node} = ''; $href->{isApp} ||= 1; } - + if ($id) { if (!$seen{$id}) { main::INFOLOG && $isInfo && $log->info("registering menuitem " . $id . " to " . $node ); @@ -522,7 +522,7 @@ sub registerPluginMenu { } # @new is the new @pluginMenus - # we do this in reverse so we get previously initialized nodes first + # we do this in reverse so we get previously initialized nodes first # you can't add an item to a node that doesn't exist :) @pluginMenus = reverse @new; @@ -535,7 +535,7 @@ sub deleteMenuItem { my $client = shift || undef; return unless $menuId; main::INFOLOG && $log->is_warn && $log->warn($menuId . " menu id slated for deletion"); - + # send a notification to delete # but also remember that this id is not to be sent my @menuDelete; @@ -551,7 +551,7 @@ sub deleteMenuItem { push @new, $href; } } - + push @menuDelete, { id => $menuId }; _notifyJive(\@menuDelete, $client, 'remove'); @@ -563,15 +563,15 @@ sub deleteMenuItem { # This used to do menu refreshes when apps may have been removed sub deleteAllMenuItems { my $client = shift || return; - + my @menuDelete; - + for my $menu ( @pluginMenus, @appMenus ) { push @menuDelete, { id => $menu->{id} }; } - + main::INFOLOG && $log->is_info && $log->info( $client->id . ' removing menu items: ' . Data::Dump::dump(\@menuDelete) ); - + _notifyJive( \@menuDelete, $client, 'remove' ); } @@ -960,7 +960,7 @@ sub alarmUpdateDays { dowDel => $day, }, }, - + }, }; push @days_menu, $day; @@ -972,7 +972,7 @@ sub alarmUpdateDays { } sub getCurrentAlarms { - + my $client = shift; my @return = (); my @alarms = Slim::Utils::Alarm->getAlarms($client); @@ -1075,7 +1075,7 @@ sub syncSettingsQuery { # Bug 16030 # when no sync players present, give message about how adding squeezeboxes could allow you to sync players } else { - + my $textarea = { textarea => $request->string('SYNC_ABOUT'), }; @@ -1124,7 +1124,7 @@ sub sleepSettingsQuery { # first make sure we're playing, and its a valid song. my $remaining = 0; - + if ($val > 0) { my $now = Time::HiRes::time(); @@ -1365,7 +1365,7 @@ sub internetRadioMenu { my $validQuery = $test_request->isValidQuery(); my @menu = (); - + if ($validQuery && $test_request->getResult('count')) { push @menu, { @@ -1407,7 +1407,7 @@ sub playerSettingsMenu { isANode => 1, weight => 35, }; - + # always add repeat push @menu, repeatSettings($client, 1); @@ -1958,32 +1958,32 @@ sub _clientId { } return $id; } - + sub _notifyJive { my ($menu, $client, $action) = @_; $action ||= 'add'; - + my $id = _clientId($client); my $menuForExport = $action eq 'add' ? _purgeMenu($menu) : $menu; - + $menuForExport = [ map { _localizeMenuItemText( $client, $_ ) } @{$menuForExport} ]; - + Slim::Control::Request::notifyFromArray( $client, [ 'menustatus', $menuForExport, $action, $id ] ); } sub howManyPlayersToSyncWith { my $client = shift; return 0 if $client->isa('Slim::Player::Disconnected'); - + my @playerSyncList = Slim::Player::Client::clients(); my $synchablePlayers = 0; - + for my $player (@playerSyncList) { # skip ourself next if ($client eq $player); # we only sync slimproto devices next if (!$player->isPlayer()); - + $synchablePlayers++; } return $synchablePlayers; @@ -1992,7 +1992,7 @@ sub howManyPlayersToSyncWith { sub getPlayersToSyncWith() { my $client = shift; my @return = (); - + # first add a descriptive line for this player push @return, { text => $client->string('SYNC_X_TO', $client->name()), @@ -2007,7 +2007,7 @@ sub getPlayersToSyncWith() { # construct the list my @syncList; my $currentlySyncedWith = 0; - + # the logic is a little tricky here...first make a pass at any sync groups that include $client if ($client->isSynced()) { $syncList[$cnt] = {}; @@ -2102,7 +2102,7 @@ sub jiveSyncCommand { if ($syncWith) { my $otherClient = Slim::Player::Client::getClient($syncWith); $otherClient->execute( [ 'sync', $client->id ] ); - + push @messages, $request->string('SYNCING_WITH', $syncWithString); } my $message = join("\n", @messages); @@ -2137,14 +2137,14 @@ sub dateQuery { # (See Request.pm "send the notification to all filters...") # An easy workaround here is to abort on any more params in @_ return if @_ > 1; - + my $request = shift; if ( $request->isNotQuery([['date']]) ) { $request->setStatusBadDispatch(); return; } - + my $newTime = $request->getParam('set') || 0; # it time is expliciely set, we'll have to notify our listeners @@ -2202,13 +2202,13 @@ sub firmwareUpgradeQuery { my $firmwareVersion = $request->getParam('firmwareVersion'); my $model = $request->getParam('machine') || 'jive'; - + # always send the upgrade url this is also used if the user opts to upgrade if ( my $url = Slim::Utils::Firmware->url($model) ) { # Bug 6828, Send relative firmware URLs for Jive versions which support it my ($cur_rev) = $firmwareVersion =~ m/\sr(\d+)/; - - # return full url when running SqueezeOS - we'll serve the direct download link from squeezenetwork + + # return full url when running some systems - we'll serve the direct download link from squeezenetwork if ( $cur_rev >= 1659 && !Slim::Utils::OSDetect->getOS()->directFirmwareDownload() ) { $request->addResult( relativeFirmwareUrl => URI->new($url)->path ); } @@ -2216,7 +2216,7 @@ sub firmwareUpgradeQuery { $request->addResult( firmwareUrl => $url ); } } - + if ( Slim::Utils::Firmware->need_upgrade( $firmwareVersion, $model ) ) { # if this is true a firmware upgrade is forced $request->addResult( firmwareUpgrade => 1 ); @@ -2298,7 +2298,7 @@ sub sleepInXHash { } sub transitionHash { - + my ($client, $val, $prefs, $strings, $thisValue) = @_; my %return = ( text => $client->string($strings->[$thisValue]), @@ -2314,7 +2314,7 @@ sub transitionHash { } sub replayGainHash { - + my ($client, $val, $prefs, $strings, $thisValue) = @_; my %return = ( text => $client->string($strings->[$thisValue]), @@ -2337,19 +2337,19 @@ sub myMusicMenu { my $client = shift; my $myMusicMenu = Slim::Menu::BrowseLibrary::getJiveMenu($client, 'myMusic', \&libraryChanged); - - + + if (!$batch) { my %newMenuItems = map {$_->{'id'} => 1} @$myMusicMenu; my @myMusicMenuDelete = map +{id => $_, node => 'myMusic'}, (grep {!$newMenuItems{$_}} keys %allMyMusicMenuItems); - + _notifyJive(\@myMusicMenuDelete, $client, 'remove'); } - + foreach (@$myMusicMenu) { $allMyMusicMenuItems{$_->{'id'}} = 1; } - + if ($batch) { return $myMusicMenu; } else { @@ -2520,7 +2520,7 @@ sub jivePresetsMenu { my $presets = $prefs->client($client)->get('presets'); my @presets_menu; - for my $preset (0..5) { + for my $preset (0..9) { my $jive_preset = $preset + 1; # is this preset currently set? my $set = ref($presets) eq 'ARRAY' && defined $presets->[$preset] ? 1 : 0; @@ -2591,7 +2591,7 @@ sub jivePresetsMenu { $request->addResult('count', scalar(@presets_menu)); $request->addResult('item_loop', \@presets_menu); $request->setStatusDone(); - + } @@ -2613,6 +2613,9 @@ sub jiveFavoritesCommand { if ( $command eq 'set_preset' ) { # XXX: why do we use a favorites_ prefix here but not above? my $preset = $request->getParam('key'); + if ( $preset == 0 ) { + $preset = 10; + } my $title = $request->getParam('favorites_title'); my $url = $request->getParam('favorites_url'); my $type = $request->getParam('favorites_type'); @@ -2634,7 +2637,7 @@ sub jiveFavoritesCommand { $request->setStatusBadDispatch(); return; } - + $client->setPreset( { slot => $preset, URL => $url, @@ -2682,15 +2685,15 @@ sub jiveFavoritesCommand { $actionItem->{'actions'}{'go'}{'params'}{'icon'} = $icon if $icon; $actionItem->{'actions'}{'go'}{'params'}{'item_id'} = $favIndex if defined($favIndex); push @favorites_menu, $actionItem; - + $request->addResult('offset', 0); $request->addResult('count', 2); $request->addResult('item_loop', \@favorites_menu); } - - + + $request->setStatusDone(); - + } sub _jiveNoResults { @@ -2745,7 +2748,7 @@ sub recentSearchMenu { actions => { go => { cmd => ['jiverecentsearches'], - }, + }, }, window => { text => $client->string('RECENT_SEARCHES'), @@ -2823,7 +2826,7 @@ sub removeExtensionProvider { # these are async so they can fetch and parse data to build a list of extensions sub extensionsQuery { my $request = shift; - + my ($type) = $request->getRequest(0) =~ /jive(applet|wallpaper|sound|patche)s/; # S:P:Extensions always appends 's' to type my $version= $request->getParam('version'); my $target = $request->getParam('target'); @@ -2928,37 +2931,37 @@ sub _extensionsQueryCB { sub appMenus { my $client = shift; my $batch = shift; - + my $isInfo = main::INFOLOG && $log->is_info; - + my $apps = $client->apps; my $menu = []; - + my $disabledPlugins = Slim::Utils::PluginManager->disabledPlugins(); my @disabled = map { $disabledPlugins->{$_}->{name} } keys %{$disabledPlugins}; - + # We want to add nodes for the following items: # My Apps (node = null) # Home menu apps (node = home) # If a home menu app is not already defined in @appMenus, # i.e. pure OPML apps such as SomaFM # create one for it using the generic OPML handler - + for my $app ( keys %{$apps} ) { next unless ref $apps->{$app} eq 'HASH'; # XXX don't crash on old style - + # Is this app supported by a local plugin? if ( my $plugin = $apps->{$app}->{plugin} ) { # Make sure it's enabled if ( my $pluginInfo = Slim::Utils::PluginManager->isEnabled($plugin) ) { - + # Get the predefined menu for this plugin if ( my ($globalMenu) = grep { ( $_->{uuid} && lc($_->{uuid}) eq lc($pluginInfo->{id}) ) || ( $_->{text} && $_->{text} eq $pluginInfo->{name} ) } @appMenus ) { main::INFOLOG && $isInfo && $log->info( "App: $app, using plugin $plugin" ); - + # Clone the existing menu and set the node my $clone = Storable::dclone($globalMenu); @@ -2970,7 +2973,7 @@ sub appMenus { # flag as an app $clone->{isApp} = 1; - + # use icon as defined by MySB to allow for white-label solutions if ( my $icon = $apps->{$app}->{icon} ) { $icon = Slim::Networking::SqueezeNetwork->url( $icon, 'external' ) unless main::NOMYSB || $icon =~ /^http/; @@ -2992,17 +2995,17 @@ sub appMenus { # For type=opml, use generic handler if ( $apps->{$app}->{type} && $apps->{$app}->{type} eq 'opml' ) { main::INFOLOG && $isInfo && $log->info( "App: $app, using generic OPML handler" ); - + my $url = ( main::NOMYSB || $apps->{$app}->{url} =~ /^http/ ) ? $apps->{$app}->{url} : Slim::Networking::SqueezeNetwork->url( $apps->{$app}->{url} ); - + my $icon = ( main::NOMYSB || $apps->{$app}->{icon} =~ /^http/ ) ? $apps->{$app}->{icon} : Slim::Networking::SqueezeNetwork->url( $apps->{$app}->{icon}, 'external' ); - + my $node = $apps->{$app}->{home_menu} == 1 ? 'home' : ''; - + push @{$menu}, { actions => { go => { @@ -3026,17 +3029,17 @@ sub appMenus { } } } - + return [] if !scalar @{$menu}; - + # Alpha sort and add weighting my $weight = 25; # After Search - + my @sorted = map { $_->{weight} = $weight++; $_ } sort { $a->{text} cmp $b->{text} } @{$menu}; - + if ( $batch ) { return \@sorted; } @@ -3047,12 +3050,12 @@ sub appMenus { sub _localizeMenuItemText { my ( $client, $item ) = @_; - + return unless $client; - + # Don't alter the global data my $clone = Storable::dclone($item); - + if ( $clone->{stringToken} ) { if ( $clone->{stringToken} eq uc( $clone->{stringToken} ) && Slim::Utils::Strings::stringExists( $clone->{stringToken} ) ) { $clone->{text} = $client->string( delete $clone->{stringToken} ); @@ -3064,14 +3067,14 @@ sub _localizeMenuItemText { elsif ( $clone->{text} && $clone->{text} eq uc( $clone->{text} ) && Slim::Utils::Strings::stringExists( $clone->{text} ) ) { $clone->{text} = $client->string( $clone->{text} ); } - + # call string() for screensaver titles if ( $clone->{screensavers} ) { for my $s ( @{ $clone->{screensavers} } ) { $s->{text} = $client->string( delete $s->{stringToken} ) if $s->{stringToken}; } } - + # call string() for input text if necessary if ( my $input = $clone->{input} ) { if ( $input->{title} && $input->{title} eq uc( $input->{title} ) ) { @@ -3082,7 +3085,7 @@ sub _localizeMenuItemText { $input->{softbutton2} = $client->string( $input->{softbutton2} ); } } - + return $clone; } diff --git a/Slim/Control/Queries.pm b/Slim/Control/Queries.pm index 5579c0fbbd9..c90f825b1eb 100644 --- a/Slim/Control/Queries.pm +++ b/Slim/Control/Queries.pm @@ -1,6 +1,6 @@ package Slim::Control::Queries; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -18,7 +18,7 @@ Slim::Control::Queries =head1 DESCRIPTION -L implements most Logitech Media Server queries and is designed to +L implements most Logitech Media Server queries and is designed to be exclusively called through Request.pm and the mechanisms it defines. Except for subscribe-able queries (such as status and serverstatus), there are no @@ -29,6 +29,7 @@ L implements most Logitech Media Server queries and is d use strict; +use File::Basename qw(basename); use Storable; use JSON::XS::VersionOneAndTwo; use Digest::MD5 qw(md5_hex); @@ -64,7 +65,7 @@ tie my %bmfCache, 'Tie::Cache::LRU::Expires', EXPIRES => 60, ENTRIES => $prefs-> sub init { my $class = shift; - + # Wipe cached data after rescan if ( !main::SCANNER ) { Slim::Control::Request::subscribe( sub { @@ -96,19 +97,19 @@ sub alarmPlaylistsQuery { my @playlistChoices; my $loopname = 'item_loop'; my $cnt = 0; - + my ($valid, $start, $end) = ( $menuMode ? (1, 0, scalar @$playlists) : $request->normalize(scalar($index), scalar($quantity), scalar @$playlists) ); for my $typeRef (@$playlists[$start..$end]) { - + my $type = $typeRef->{type}; my @choices = (); my $aref = $typeRef->{items}; - + for my $choice (@$aref) { if ($menuMode) { - my $radio = ( + my $radio = ( ( $currentSetting && $currentSetting eq $choice->{url} ) || ( !defined $choice->{url} && !defined $currentSetting ) ); @@ -129,7 +130,7 @@ sub alarmPlaylistsQuery { title => $choice->{title}, cmd => [ 'playlist', 'preview' ], params => { - url => $choice->{url}, + url => $choice->{url}, title => $choice->{title}, }, }, @@ -140,15 +141,15 @@ sub alarmPlaylistsQuery { cmd => [ 'play' ], }; } - - + + if ($typeRef->{singleItem}) { $subitem->{'nextWindow'} = 'refresh'; } - + push @choices, $subitem; } - + else { $request->addResultLoop($loopname, $cnt, 'category', $type); $request->addResultLoop($loopname, $cnt, 'title', $choice->{title}); @@ -167,11 +168,11 @@ sub alarmPlaylistsQuery { item_loop => \@choices, }; $request->setResultLoopHash($loopname, $cnt, $item); - + $cnt++; } } - + $request->addResult("offset", $start); $request->addResult("count", $cnt); $request->addResult('window', { textareaToken => 'SLIMBROWSER_ALARM_SOUND_HELP' } ); @@ -186,22 +187,22 @@ sub alarmsQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); my $index = $request->getParam('_index'); my $quantity = $request->getParam('_quantity'); my $filter = $request->getParam('filter'); my $alarmDOW = $request->getParam('dow'); - + # being nice: we'll still be accepting 'defined' though this doesn't make sense any longer if ($request->paramNotOneOfIfDefined($filter, ['all', 'defined', 'enabled'])) { $request->setStatusBadParams(); return; } - + $request->addResult('fade', $prefs->client($client)->get('alarmfadeseconds')); - + $filter = 'enabled' if !defined $filter; my @alarms = grep { @@ -220,7 +221,7 @@ sub alarmsQuery { my $loopname = 'alarms_loop'; my $cnt = 0; - + for my $alarm (@alarms[$start..$end]) { my @dow; @@ -255,9 +256,9 @@ sub albumsQuery { $request->setStatusNotDispatchable(); return; } - + my $sqllog = main::DEBUGLOG && logger('database.sql'); - + # get our parameters my $client = $request->client(); my $index = $request->getParam('_index'); @@ -275,7 +276,7 @@ sub albumsQuery { my $sort = $request->getParam('sort') || ($roleID ? 'artistalbum' : 'album'); my $ignoreNewAlbumsCache = $search || $compilation || $contributorID || $genreID || $trackID || $albumID || $year || Slim::Music::Import->stillScanning(); - + # FIXME: missing genrealbum, genreartistalbum if ($request->paramNotOneOfIfDefined($sort, ['new', 'album', 'artflow', 'artistalbum', 'yearalbum', 'yearartistalbum', 'random' ])) { $request->setStatusBadParams(); @@ -283,7 +284,7 @@ sub albumsQuery { } my $collate = Slim::Utils::OSDetect->getOS()->sqlHelperClass()->collate(); - + my $sql = 'SELECT %s FROM albums '; my $c = { 'albums.id' => 1, 'albums.titlesearch' => 1, 'albums.titlesort' => 1 }; my $w = []; @@ -292,7 +293,7 @@ sub albumsQuery { my $limit; my $page_key = "SUBSTR(albums.titlesort,1,1)"; my $newAlbumsCacheKey = 'newAlbumIds' . Slim::Music::Import->lastScanTime . Slim::Music::VirtualLibraries->getLibraryIdForClient($client); - + # Normalize and add any search parameters if ( defined $trackID ) { $sql .= 'JOIN tracks ON tracks.album = albums.id '; @@ -312,10 +313,10 @@ sub albumsQuery { search => $search, type => 'album', }); - + $sql = 'SELECT %s FROM albumsSearch, albums '; unshift @{$w}, "albums.id = albumsSearch.id"; - + if ($tags ne 'CC') { $order_by = $sort = "albumsSearch.fulltextweight DESC, LENGTH(albums.titlesearch)"; } @@ -326,7 +327,7 @@ sub albumsQuery { push @{$w}, '(' . join( ' OR ', map { 'albums.titlesearch LIKE ?' } @{ $strings->[0] } ) . ')'; push @{$p}, @{ $strings->[0] }; } - else { + else { push @{$w}, 'albums.titlesearch LIKE ?'; push @{$p}, @{$strings}; } @@ -352,7 +353,7 @@ sub albumsQuery { } elsif ($prefs->get('useUnifiedArtistsList')) { @roles = ( 'ARTIST', 'TRACKARTIST', 'ALBUMARTIST' ); - + # Loop through each pref to see if the user wants to show that contributor role. foreach (Slim::Schema::Contributor->contributorRoles) { if ($prefs->get(lc($_) . 'InArtists')) { @@ -363,7 +364,7 @@ sub albumsQuery { else { @roles = Slim::Schema::Contributor->contributorRoles(); } - } + } } elsif ($roleID) { $sql .= 'JOIN contributor_album ON contributor_album.album = albums.id '; @@ -371,22 +372,22 @@ sub albumsQuery { @roles = split /,/, $roleID; push @roles, 'ARTIST' if $roleID eq 'ALBUMARTIST' && !$prefs->get('useUnifiedArtistsList'); } - + if (scalar @roles) { push @{$p}, map { Slim::Schema::Contributor->typeToRole($_) } @roles; push @{$w}, 'contributor_album.role IN (' . join(', ', map {'?'} @roles) . ')'; - + $sql .= 'JOIN contributors ON contributors.id = contributor_album.contributor '; } elsif ( $sort =~ /artflow|artistalbum/) { $sql .= 'JOIN contributors ON contributors.id = albums.contributor '; } - + if ( $sort eq 'new' ) { $sql .= 'JOIN tracks ON tracks.album = albums.id '; $limit = $prefs->get('browseagelimit') || 100; $order_by = "tracks.timestamp desc"; - + # Force quantity to not exceed max if ( $quantity && $quantity > $limit ) { $quantity = $limit; @@ -395,7 +396,7 @@ sub albumsQuery { # cache the most recent album IDs - need to query the tracks table, which is expensive if ( !$ignoreNewAlbumsCache ) { my $ids = $cache->{$newAlbumsCacheKey} || []; - + if (!scalar @$ids) { my $_cache = Slim::Utils::Cache->new; $ids = $_cache->get($newAlbumsCacheKey) || []; @@ -420,7 +421,7 @@ sub albumsQuery { # get the list of album IDs ordered by timestamp $ids = Slim::Schema->dbh->selectcol_arrayref( $countSQL, { Slice => {} } ) unless scalar @$ids; - + $cache->{$newAlbumsCacheKey} = $ids; $_cache->set($newAlbumsCacheKey, $ids, 86400 * 7) if scalar @$ids; } @@ -458,7 +459,7 @@ sub albumsQuery { } elsif ( $sort eq 'random' ) { $limit = $prefs->get('itemsPerPage'); - + # Force quantity to not exceed max if ( $quantity && $quantity > $limit ) { $quantity = $limit; @@ -472,12 +473,12 @@ sub albumsQuery { push @{$w}, 'albums.id IN (SELECT library_album.album FROM library_album WHERE library_album.library = ?)'; push @{$p}, $libraryID; } - + if (defined $year) { push @{$w}, 'albums.year = ?'; push @{$p}, $year; } - + if (defined $genreID) { my @genreIDs = split(/,/, $genreID); $sql .= 'JOIN tracks ON tracks.album = albums.id ' unless $sql =~ /JOIN tracks/; @@ -485,7 +486,7 @@ sub albumsQuery { push @{$w}, 'genre_track.genre IN (' . join(', ', map {'?'} @genreIDs) . ')'; push @{$p}, @genreIDs; } - + if (defined $compilation) { if ($compilation == 1) { push @{$w}, 'albums.compilation = 1'; @@ -495,32 +496,32 @@ sub albumsQuery { } } } - + if ( $tags =~ /l/ ) { # title/disc/discc is needed to construct (N of M) title map { $c->{$_} = 1 } qw(albums.title albums.disc albums.discc); } - + if ( $tags =~ /y/ ) { $c->{'albums.year'} = 1; } - + if ( $tags =~ /j/ ) { $c->{'albums.artwork'} = 1; } - + if ( $tags =~ /t/ ) { $c->{'albums.title'} = 1; } - + if ( $tags =~ /i/ ) { $c->{'albums.disc'} = 1; } - + if ( $tags =~ /q/ ) { $c->{'albums.discc'} = 1; } - + if ( $tags =~ /w/ ) { $c->{'albums.compilation'} = 1; } @@ -546,26 +547,26 @@ sub albumsQuery { } } $c->{'contributors.name'} = 1; - + # if albums for a specific contributor are requested, then we need the album's contributor, too $c->{'albums.contributor'} = $contributorID; } - + if ( $tags =~ /s/ ) { $c->{'albums.titlesort'} = 1; } - + if ( @{$w} ) { $sql .= 'WHERE '; my $s .= join( ' AND ', @{$w} ); $s =~ s/\%/\%\%/g; $sql .= $s . ' '; } - + my $dbh = Slim::Schema->dbh; $sql .= "GROUP BY albums.id "; - + if ($page_key && $tags =~ /Z/) { my $pageSql = "SELECT n, count(1) FROM (" . sprintf($sql, "$page_key AS n") @@ -575,38 +576,43 @@ sub albumsQuery { $sqllog->debug( "Albums indexList query: $pageSql / " . Data::Dump::dump($p) ); } - $request->addResult('indexList', $dbh->selectall_arrayref($pageSql, undef, @{$p})); - + $request->addResult('indexList', [ + map { + utf8::decode($_->[0]); + $_; + } @{ $dbh->selectall_arrayref($pageSql, undef, @{$p}) } + ]); + if ($tags =~ /ZZ/) { $request->setStatusDone(); return } } - + $sql .= "ORDER BY $order_by " unless $tags eq 'CC'; - + # Add selected columns # Bug 15997, AS mapping needed for MySQL my @cols = sort keys %{$c}; $sql = sprintf $sql, join( ', ', map { $_ . " AS '" . $_ . "'" } @cols ); - + my $stillScanning = Slim::Music::Import->stillScanning(); - + # Get count of all results, the count is cached until the next rescan done event - my $cacheKey = md5_hex($sql . join( '', @{$p} ) . Slim::Music::VirtualLibraries->getLibraryIdForClient($client) . ($search || '')); - + my $cacheKey = md5_hex($sql . join( '', @{$p} ) . Slim::Music::VirtualLibraries->getLibraryIdForClient($client) . (Slim::Utils::Text::ignoreCase($search, 1) || '')); + if ( $sort eq 'new' && $cache->{$newAlbumsCacheKey} && !$ignoreNewAlbumsCache ) { my $albumCount = scalar @{$cache->{$newAlbumsCacheKey}}; $albumCount = $limit if ($limit && $limit < $albumCount); $cache->{$cacheKey} ||= $albumCount; $limit = undef; } - + my $countsql = $sql; $countsql .= ' LIMIT ' . $limit if $limit; - + my $count = $cache->{$cacheKey}; - + if ( !$count ) { my $total_sth = $dbh->prepare_cached( qq{ SELECT COUNT(1) FROM ( $countsql ) AS t1 @@ -615,7 +621,7 @@ sub albumsQuery { if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Albums totals query: $countsql / " . Data::Dump::dump($p) ); } - + $total_sth->execute( @{$p} ); ($count) = $total_sth->fetchrow_array(); $total_sth->finish; @@ -645,7 +651,7 @@ sub albumsQuery { # It looks silly to go to Madonna->No album and see the # picture of '2 Unlimited'. my $noAlbumName = $request->string('NO_ALBUM'); - + # Limit the real query if ($limit && !$quantity) { $quantity = "$limit"; @@ -662,7 +668,7 @@ sub albumsQuery { my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p} ); - + # Bind selected columns in order my $i = 1; for my $col ( @cols ) { @@ -682,7 +688,7 @@ sub albumsQuery { my ($contributorSql, $contributorSth, $contributorNameSth); if ( $tags =~ /(?:aa|SS)/ ) { my @roles = ( 'ARTIST', 'ALBUMARTIST' ); - + if ($prefs->get('useUnifiedArtistsList')) { # Loop through each pref to see if the user wants to show that contributor role. foreach (Slim::Schema::Contributor->contributorRoles) { @@ -696,19 +702,19 @@ sub albumsQuery { SELECT GROUP_CONCAT(contributors.name, ',') AS name, GROUP_CONCAT(contributors.id, ',') AS id FROM contributor_album JOIN contributors ON contributors.id = contributor_album.contributor - WHERE contributor_album.album = ? AND contributor_album.role IN (%s) + WHERE contributor_album.album = ? AND contributor_album.role IN (%s) GROUP BY contributor_album.role ORDER BY contributor_album.role DESC }, join(',', map { Slim::Schema::Contributor->typeToRole($_) } @roles) ); } - + my $vaObjId = Slim::Schema->variousArtistsObject->id; - + while ( $sth->fetch ) { - + utf8::decode( $c->{'albums.title'} ) if exists $c->{'albums.title'}; - - $request->addResultLoop($loopname, $chunkCount, 'id', $c->{'albums.id'}); + $request->addResultLoop($loopname, $chunkCount, 'id', $c->{'albums.id'}); + $tags =~ /l/ && $request->addResultLoop($loopname, $chunkCount, 'album', $construct_title->()); $tags =~ /y/ && $request->addResultLoopIfValueDefined($loopname, $chunkCount, 'year', $c->{'albums.year'}); $tags =~ /j/ && $request->addResultLoopIfValueDefined($loopname, $chunkCount, 'artwork_track_id', $c->{'albums.artwork'}); @@ -753,10 +759,10 @@ sub albumsQuery { if ( $contributorSql && $c->{'albums.contributor'} != $vaObjId && !$c->{'albums.compilation'} ) { $contributorSth ||= $dbh->prepare_cached($contributorSql); $contributorSth->execute($c->{'albums.id'}); - + my $contributor = $contributorSth->fetchrow_hashref; $contributorSth->finish; - + # XXX - what if the artist name itself contains ','? if ( $tags =~ /aa/ && $contributor->{name} ) { utf8::decode($contributor->{name}); @@ -767,9 +773,9 @@ sub albumsQuery { $request->addResultLoopIfValueDefined($loopname, $chunkCount, 'artist_ids', $contributor->{id}); } } - + $chunkCount++; - + main::idleStreams() if !($chunkCount % 5); } @@ -793,9 +799,9 @@ sub artistsQuery { $request->setStatusNotDispatchable(); return; } - + my $sqllog = main::DEBUGLOG && logger('database.sql'); - + # get our parameters my $client = $request->client(); my $index = $request->getParam('_index'); @@ -810,12 +816,12 @@ sub artistsQuery { my $roleID = $request->getParam('role_id'); my $libraryID= Slim::Music::VirtualLibraries->getRealId($request->getParam('library_id')); my $tags = $request->getParam('tags') || ''; - + # treat contributors for albums with only one ARTIST but no ALBUMARTIST the same my $aa_merge = $roleID && $roleID eq 'ALBUMARTIST' && !$prefs->get('useUnifiedArtistsList'); my $va_pref = $prefs->get('variousArtistAutoIdentification') && $prefs->get('useUnifiedArtistsList'); - + my $sql = 'SELECT %s FROM contributors '; my $sql_va = 'SELECT COUNT(*) FROM albums '; my $w = []; @@ -825,11 +831,11 @@ sub artistsQuery { my $rs; my $cacheKey; - + my $collate = Slim::Utils::OSDetect->getOS()->sqlHelperClass()->collate(); my $sort = "contributors.namesort $collate"; - # Manage joins + # Manage joins if (defined $trackID) { $sql .= 'JOIN contributor_track ON contributor_track.contributor = contributors.id '; push @{$w}, 'contributor_track.track = ?'; @@ -846,10 +852,10 @@ sub artistsQuery { search => $search, type => 'contributor', }); - + $sql = 'SELECT %s FROM artistsSearch, contributors '; unshift @{$w}, "contributors.id = artistsSearch.id"; - + if ($tags ne 'CC') { $sort = "artistsSearch.fulltextweight DESC, LENGTH(contributors.name), $sort"; } @@ -866,7 +872,7 @@ sub artistsQuery { else { $roles = [ map { Slim::Schema::Contributor->typeToRole($_) } Slim::Schema::Contributor->contributorRoles() ]; } - + if ( defined $genreID ) { my @genreIDs = split(/,/, $genreID); @@ -875,20 +881,20 @@ sub artistsQuery { $sql .= 'JOIN genre_track ON genre_track.track = tracks.id '; push @{$w}, 'genre_track.genre IN (' . join(', ', map {'?'} @genreIDs) . ')'; push @{$p}, @genreIDs; - + # Adjust VA check to check for VA artists in this genre $sql_va .= 'JOIN tracks ON tracks.album = albums.id '; $sql_va .= 'JOIN genre_track ON genre_track.track = tracks.id '; push @{$w_va}, 'genre_track.genre = ?'; push @{$p_va}, $genreID; } - + if ( !defined $search ) { if ( $sql !~ /JOIN contributor_track/ ) { $sql .= 'JOIN contributor_album ON contributor_album.contributor = contributors.id '; } } - + # XXX - why would we not filter by role, as drilling down would filter anyway, potentially leading to empty resultsets? # make sure we don't miss the VA object, as it might not have any of the roles we're looking for -mh #if ( !defined $search ) { @@ -901,47 +907,47 @@ sub artistsQuery { } push @{$w}, '(contributor_album.role IN (' . join( ',', @{$roles} ) . ') ' . ($search ? 'OR contributors.id = ? ' : '') . ') '; } - + push @{$p}, Slim::Schema->variousArtistsObject->id if $search; - + if ( $va_pref || $aa_merge ) { # Don't include artists that only appear on compilations if ( $sql =~ /JOIN tracks/ ) { # If doing an artists-in-genre query, we are much better off joining through albums $sql .= 'JOIN albums ON albums.id = tracks.album '; - } + } else { if ( $sql !~ /JOIN contributor_album/ ) { $sql .= 'JOIN contributor_album ON contributor_album.contributor = contributors.id '; } $sql .= 'JOIN albums ON contributor_album.album = albums.id '; } - + push @{$w}, '(albums.compilation IS NULL OR albums.compilation = 0' . ($va_pref ? '' : ' OR contributors.id = ' . Slim::Schema->variousArtistsObject->id) . ')'; } #} - + if (defined $albumID || defined $year) { if ( $sql !~ /JOIN contributor_album/ ) { $sql .= 'JOIN contributor_album ON contributor_album.contributor = contributors.id '; } - + if ( $sql !~ /JOIN albums/ ) { $sql .= 'JOIN albums ON contributor_album.album = albums.id '; } - + if (defined $albumID) { push @{$w}, 'albums.id = ?'; push @{$p}, $albumID; - + push @{$w_va}, 'albums.id = ?'; push @{$p_va}, $albumID; } - + if (defined $year) { push @{$w}, 'albums.year = ?'; push @{$p}, $year; - + push @{$w_va}, 'albums.year = ?'; push @{$p_va}, $year; } @@ -953,7 +959,7 @@ sub artistsQuery { push @{$w}, '(' . join( ' OR ', map { 'contributors.namesearch LIKE ?' } @{ $strings->[0] } ) . ')'; push @{$p}, @{ $strings->[0] }; } - else { + else { push @{$w}, 'contributors.namesearch LIKE ?'; push @{$p}, @{$strings}; } @@ -965,14 +971,14 @@ sub artistsQuery { push @{$p}, $libraryID; } } - + if ( @{$w} ) { $sql .= 'WHERE '; my $s = join( ' AND ', @{$w} ); $s =~ s/\%/\%\%/g; $sql .= $s . ' '; } - + my $dbh = Slim::Schema->dbh; # Various artist handling. Don't do if pref is off, or if we're @@ -986,13 +992,13 @@ sub artistsQuery { $sql_va .= join( ' AND ', @{$w_va} ); $sql_va .= ' '; } - + if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Artists query VA count: $sql_va / " . Data::Dump::dump($p_va) ); } - + my $total_sth = $dbh->prepare_cached( $sql_va ); - + $total_sth->execute( @{$p_va} ); ($count_va) = $total_sth->fetchrow_array(); $total_sth->finish; @@ -1003,9 +1009,12 @@ sub artistsQuery { my $pageSql = sprintf($sql, "SUBSTR(contributors.namesort,1,1), count(distinct contributors.id)") . "GROUP BY SUBSTR(contributors.namesort,1,1) ORDER BY contributors.namesort $collate"; $indexList = $dbh->selectall_arrayref($pageSql, undef, @{$p}); - + foreach (@$indexList) { + utf8::decode($_->[0]) + } + unshift @$indexList, ['#' => 1] if $indexList && $count_va; - + if ($tags =~ /ZZ/) { $request->addResult('indexList', $indexList) if $indexList; $request->setStatusDone(); @@ -1015,34 +1024,34 @@ sub artistsQuery { $sql = sprintf($sql, 'contributors.id, contributors.name, contributors.namesort') . 'GROUP BY contributors.id '; - + $sql .= "ORDER BY $sort " unless $tags eq 'CC'; - + my $stillScanning = Slim::Music::Import->stillScanning(); - + # Get count of all results, the count is cached until the next rescan done event - $cacheKey = md5_hex($sql . join( '', @{$p} ) . Slim::Music::VirtualLibraries->getLibraryIdForClient($client) . ($search || '')); - + $cacheKey = md5_hex($sql . join( '', @{$p} ) . Slim::Music::VirtualLibraries->getLibraryIdForClient($client) . (Slim::Utils::Text::ignoreCase($search, 1) || '')); + my $count = $cache->{$cacheKey}; - + if ( !$count ) { my $total_sth = $dbh->prepare_cached( qq{ SELECT COUNT(1) FROM ( $sql ) AS t1 } ); - + if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Artists totals query: $sql / " . Data::Dump::dump($p) ); } - + $total_sth->execute( @{$p} ); ($count) = $total_sth->fetchrow_array(); $total_sth->finish; } - + if ( !$stillScanning ) { $cache->{$cacheKey} = $count; } - + my $totalCount = $count || 0; if ( $count_va ) { @@ -1054,13 +1063,13 @@ sub artistsQuery { } # now build the result - + if ($stillScanning) { $request->addResult('rescan', 1); } $count += 0; - + # If count is 0 but count_va is 1, set count to 1 because # we'll still have a VA item to add to the results if ( $count_va && !$count ) { @@ -1073,11 +1082,11 @@ sub artistsQuery { if ( $count_va && $index == 0 && $quantity == 0 ) { $valid = 1; } - + my $loopname = 'artists_loop'; my $chunkCount = 0; - if ($valid && $tags ne 'CC') { + if ($valid && $tags ne 'CC') { # Limit the real query if ( $index =~ /^\d+$/ && $quantity =~ /^\d+$/ ) { $sql .= "LIMIT ?,? "; @@ -1090,13 +1099,13 @@ sub artistsQuery { my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p} ); - + my ($id, $name, $namesort); $sth->bind_columns( \$id, \$name, \$namesort ); - + my $process = sub { $id += 0; - + utf8::decode($name); utf8::decode($namesort); @@ -1109,14 +1118,14 @@ sub artistsQuery { } $chunkCount++; - + main::idleStreams() if !($chunkCount % 10); }; - + # Add VA item first if necessary if ( $count_va ) { my $vaObj = Slim::Schema->variousArtistsObject; - + # bug 15328 - get the VA name in the language requested by the client # but only do so if the user isn't using a custom name my $vaName = $vaObj->name; @@ -1125,24 +1134,24 @@ sub artistsQuery { $vaName = $request->string('VARIOUSARTISTS'); $vaNamesort = Slim::Utils::Text::ignoreCaseArticles($vaName); } - + $id = $vaObj->id; $name = $vaName; $namesort = $vaNamesort; - + $process->(); } while ( $sth->fetch ) { $process->(); } - + } - + $request->addResult('indexList', $indexList) if $indexList; $request->addResult('count', $totalCount); - + $request->setStatusDone(); } @@ -1156,27 +1165,27 @@ sub cursonginfoQuery { $request->setStatusBadDispatch(); return; } - + my $client = $request->client(); # get the query my $method = $request->getRequest(0); my $url = Slim::Player::Playlist::url($client); - + if (defined $url) { if ($method eq 'path') { - + $request->addResult("_$method", $url); } elsif ($method eq 'remote') { - - $request->addResult("_$method", + + $request->addResult("_$method", Slim::Music::Info::isRemoteURL($url)); - + } elsif ($method eq 'current_title') { - - $request->addResult("_$method", + + $request->addResult("_$method", Slim::Music::Info::getCurrentTitle($client, $url)); } else { @@ -1186,7 +1195,7 @@ sub cursonginfoQuery { $url, 'dalg', # tags needed for our entities ); - + if (defined $songData->{$method}) { $request->addResult("_$method", $songData->{$method}); } @@ -1206,12 +1215,12 @@ sub connectedQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); - + $request->addResult('_connected', $client->connected() || 0); - + $request->setStatusDone(); } @@ -1224,7 +1233,7 @@ sub debugQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $category = $request->getParam('_debugflag'); @@ -1235,11 +1244,11 @@ sub debugQuery { } my $categories = Slim::Utils::Log->allCategories; - + if (defined $categories->{$category}) { - + $request->addResult('_value', $categories->{$category}); - + $request->setStatusDone(); } else { @@ -1257,15 +1266,15 @@ sub displayQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); - + my $parsed = $client->curLines(); $request->addResult('_line1', $parsed->{line}[0] || ''); $request->addResult('_line2', $parsed->{line}[1] || ''); - + $request->setStatusDone(); } @@ -1278,13 +1287,13 @@ sub displaynowQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); $request->addResult('_line1', $client->prevline1()); $request->addResult('_line2', $client->prevline2()); - + $request->setStatusDone(); } @@ -1298,7 +1307,7 @@ sub displaystatusQuery_filter { # retrieve the clientid, abort if not about us my $clientid = $request->clientid() || return 0; - my $myclientid = $self->clientid() || return 0; + my $myclientid = $self->clientid() || return 0; return 0 if $clientid ne $myclientid; my $subs = $self->getParam('subscribe'); @@ -1331,7 +1340,7 @@ sub displaystatusQuery_filter { sub displaystatusQuery { my $request = shift; - + main::DEBUGLOG && $log->debug("displaystatusQuery()"); # check this is the correct query @@ -1391,19 +1400,19 @@ sub displaystatusQuery { $request->addResult('display', $parts->{'jive'} ); } } else { - my $display = { + my $display = { 'text' => $screen1->{'line'} || $screen1->{'center'} }; - + $display->{duration} = $duration if $duration; - + $request->addResult('display', $display); } } } elsif ($subs =~ /showbriefly|update|bits|all/) { # new subscription request - add subscription, assume cli or jive format for the moment - $request->privateData({ 'format' => $request->source eq 'CLI' ? 'cli' : 'jive' }); + $request->privateData({ 'format' => $request->source eq 'CLI' ? 'cli' : 'jive' }); my $client = $request->client; @@ -1422,7 +1431,7 @@ sub displaystatusQuery { # swap to emulated display $client->display->forgetDisplay(); $client->display( Slim::Display::EmulatedSqueezebox2->new($client) ); - $client->display->init; + $client->display->init; # register ourselves for execution and a cleanup function to swap the display class back $request->registerAutoExecute(0, \&displaystatusQuery_filter, \&_displaystatusCleanupEmulated); } @@ -1462,7 +1471,7 @@ sub displaystatusQuery { $client->update; } } - + $request->setStatusDone(); } @@ -1493,9 +1502,9 @@ sub genresQuery { $request->setStatusNotDispatchable(); return; } - + my $sqllog = main::DEBUGLOG && logger('database.sql'); - + # get our parameters my $client = $request->client(); my $index = $request->getParam('_index'); @@ -1508,7 +1517,7 @@ sub genresQuery { my $genreID = $request->getParam('genre_id'); my $libraryID = Slim::Music::VirtualLibraries->getRealId($request->getParam('library_id')); my $tags = $request->getParam('tags') || ''; - + my $sql = 'SELECT %s FROM genres '; my $w = []; my $p = []; @@ -1520,7 +1529,7 @@ sub genresQuery { push @{$w}, '(' . join( ' OR ', map { 'genres.namesearch LIKE ?' } @{ $strings->[0] } ) . ')'; push @{$p}, @{ $strings->[0] }; } - else { + else { push @{$w}, 'genres.namesearch LIKE ?'; push @{$p}, @{$strings}; } @@ -1540,7 +1549,7 @@ sub genresQuery { else { # ignore those if we have a track. if (defined $contributorID) { - + # handle the case where we're asked for the VA id => return compilations if ($contributorID == Slim::Schema->variousArtistsObject->id) { $sql .= 'JOIN genre_track ON genres.id = genre_track.genre '; @@ -1556,13 +1565,13 @@ sub genresQuery { push @{$p}, $contributorID; } } - + if ( $libraryID ) { $sql .= 'JOIN library_genre ON library_genre.genre = genres.id '; push @{$w}, 'library_genre.library = ?'; push @{$p}, $libraryID; } - + if (defined $albumID || defined $year) { if ( $sql !~ /JOIN genre_track/ ) { $sql .= 'JOIN genre_track ON genres.id = genre_track.genre '; @@ -1570,7 +1579,7 @@ sub genresQuery { if ( $sql !~ /JOIN tracks/ ) { $sql .= 'JOIN tracks ON genre_track.track = tracks.id '; } - + if (defined $albumID) { push @{$w}, 'tracks.album = ?'; push @{$p}, $albumID; @@ -1581,53 +1590,58 @@ sub genresQuery { } } } - + if ( @{$w} ) { $sql .= 'WHERE '; my $s = join( ' AND ', @{$w} ); $s =~ s/\%/\%\%/g; $sql .= $s . ' '; } - + my $dbh = Slim::Schema->dbh; - + my $collate = Slim::Utils::OSDetect->getOS()->sqlHelperClass()->collate(); - + if ($tags =~ /Z/) { my $pageSql = sprintf($sql, "SUBSTR(genres.namesort,1,1), count(distinct genres.id)") . "GROUP BY SUBSTR(genres.namesort,1,1) ORDER BY genres.namesort $collate"; - $request->addResult('indexList', $dbh->selectall_arrayref($pageSql, undef, @{$p})); + $request->addResult('indexList', [ + map { + utf8::decode($_->[0]); + $_; + } @{ $dbh->selectall_arrayref($pageSql, undef, @{$p}) } + ]); if ($tags =~ /ZZ/) { $request->setStatusDone(); return } } - + $sql = sprintf($sql, 'DISTINCT(genres.id), genres.name, genres.namesort'); $sql .= "ORDER BY genres.namesort $collate" unless $tags eq 'CC'; - + my $stillScanning = Slim::Music::Import->stillScanning(); - + # Get count of all results, the count is cached until the next rescan done event my $cacheKey = md5_hex($sql . join( '', @{$p} ) . Slim::Music::VirtualLibraries->getLibraryIdForClient($client)); - + my $count = $cache->{$cacheKey}; if ( !$count ) { my $total_sth = $dbh->prepare_cached( qq{ SELECT COUNT(1) FROM ( $sql ) AS t1 } ); - + $total_sth->execute( @{$p} ); ($count) = $total_sth->fetchrow_array(); $total_sth->finish; } - + if ( !$stillScanning ) { $cache->{$cacheKey} = $count; } - + # now build the result - + if ($stillScanning) { $request->addResult('rescan', 1); } @@ -1640,7 +1654,7 @@ sub genresQuery { my $loopname = 'genres_loop'; my $chunkCount = 0; - + # Limit the real query if ( $index =~ /^\d+$/ && $quantity =~ /^\d+$/ ) { $sql .= "LIMIT $index, $quantity "; @@ -1652,24 +1666,24 @@ sub genresQuery { my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p} ); - + my ($id, $name, $namesort); $sth->bind_columns( \$id, \$name, \$namesort ); - + while ( $sth->fetch ) { $id += 0; - + utf8::decode($name) if $name; utf8::decode($namesort) if $namesort; - + my $textKey = substr($namesort, 0, 1); - + $request->addResultLoop($loopname, $chunkCount, 'id', $id); $request->addResultLoop($loopname, $chunkCount, 'genre', $name); $tags =~ /s/ && $request->addResultLoop($loopname, $chunkCount, 'textkey', $textKey); $chunkCount++; - + main::idleStreams() if !($chunkCount % 5); } } @@ -1688,25 +1702,25 @@ sub getStringQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $tokenlist = $request->getParam('_tokens'); foreach my $token (split /,/, $tokenlist) { - + # check whether string exists or not, to prevent stack dumps if # client queries inexistent string if (Slim::Utils::Strings::stringExists($token)) { - + $request->addResult($token, $request->string($token)); } - + else { - + $request->addResult($token, ''); } } - + $request->setStatusDone(); } @@ -1719,15 +1733,15 @@ sub infoTotalQuery { $request->setStatusBadDispatch(); return; } - + if (!Slim::Schema::hasLibrary()) { $request->setStatusNotDispatchable(); return; } - + # get our parameters my $entity = $request->getRequest(2); - + my $totals = Slim::Schema->totals($request->client) if $entity ne 'duration'; if ($entity eq 'albums') { @@ -1745,7 +1759,7 @@ sub infoTotalQuery { elsif ($entity eq 'duration') { $request->addResult("_$entity", Slim::Schema->totalTime($request->client)); } - + $request->setStatusDone(); } @@ -1758,37 +1772,37 @@ sub irenableQuery { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); $request->addResult('_irenable', $client->irenable()); - + $request->setStatusDone(); } sub librariesQuery { my $request = shift; - + if ($request->isNotQuery([['libraries']])) { $request->setStatusBadDispatch(); return; } - + if ( $request->isQuery([['libraries'], ['getid']]) && (my $client = $request->client) ) { my $id = Slim::Music::VirtualLibraries->getLibraryIdForClient($client) || 0; $request->addResult('id', $id); $request->addResult('name', Slim::Music::VirtualLibraries->getNameForId($id, $client)) if $id; } else { - my $i = 0; + my $i = 0; while ( my ($id, $args) = each %{ Slim::Music::VirtualLibraries->getLibraries() } ) { $request->addResultLoop('folder_loop', $i, 'id', $id); $request->addResultLoop('folder_loop', $i, 'name', $args->{name}); $i++; } } - + $request->setStatusDone(); } @@ -1800,12 +1814,12 @@ sub linesperscreenQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); $request->addResult('_linesperscreen', $client->linesPerScreen()); - + $request->setStatusDone(); } @@ -1831,7 +1845,7 @@ sub mixerQuery { } else { $request->addResult("_$entity", $client->$entity()); } - + $request->setStatusDone(); } @@ -1844,12 +1858,12 @@ sub modeQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); $request->addResult('_mode', Slim::Player::Source::playmode($client)); - + $request->setStatusDone(); } @@ -1859,7 +1873,7 @@ sub musicfolderQuery { sub mediafolderQuery { my $request = shift; - + main::INFOLOG && $log->info("mediafolderQuery()"); # check this is the correct query. @@ -1867,7 +1881,7 @@ sub mediafolderQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); my $index = $request->getParam('_index'); @@ -1877,12 +1891,12 @@ sub mediafolderQuery { my $url = $request->getParam('url'); my $type = $request->getParam('type') || ''; my $tags = $request->getParam('tags') || ''; - + # duration is not available for anything but audio files $tags =~ s/d// if $type && $type ne 'audio'; - + my ($sql, $volatileUrl); - + # Bug 17436, don't allow BMF if a scan is running, browse without storing tracks in database instead if (Slim::Music::Import->stillScanning()) { $volatileUrl = 1; @@ -1893,26 +1907,26 @@ sub mediafolderQuery { $volatileUrl = $url; $volatileUrl =~ s/^tmp/file/; } - + # url overrides any folderId my $params = (); my $mediaDirs = Slim::Utils::Misc::getMediaDirs($type || 'audio'); - + $params->{recursive} = $request->getParam('recursive'); - + # add "volatile" folders which are not scanned, to be browsed and played on the fly - push @$mediaDirs, map { + push @$mediaDirs, map { my $url = Slim::Utils::Misc::fileURLFromPath($_); $url =~ s/^file/tmp/; $url; } @{ Slim::Utils::Misc::getInactiveMediaDirs() } if !$type || $type eq 'audio'; - + my ($topLevelObj, $items, $count, $topPath, $realName); - + my $bmfUrlForName = $cache->{bmfUrlForName} || {}; - + my $highmem = $prefs->get('dbhighmem'); - + my $filter = sub { # if a $sth is passed, we'll do a quick lookup to check existence only, not returning an actual object if possible my ($filename, $topPath, $sth) = @_; @@ -1920,7 +1934,7 @@ sub mediafolderQuery { my $url = $bmfUrlForName->{$filename . $topPath}; if (!$url) { $url ||= Slim::Utils::Misc::fixPath($filename, $topPath) || ''; - + # keep a cache of the mapping in memory if we can afford it if ($highmem && $url) { $bmfUrlForName->{$filename . $topPath} ||= $url; @@ -1933,7 +1947,7 @@ sub mediafolderQuery { if (main::ISWINDOWS && Slim::Music::Info::isWinShortcut($url)) { ($realName, $url) = Slim::Utils::OS::Win32->getShortcut($url); } - + elsif (main::ISMAC) { if ( my $alias = Slim::Utils::Misc::pathFromMacAlias($url) ) { $url = $alias; @@ -1944,14 +1958,14 @@ sub mediafolderQuery { # don't create the dir objects in the first pass - we can create them later when paging through the list # only run a quick, relatively cheap test on the type of the URL $sth->execute($url); - + my $itemDetails = $sth->fetchrow_hashref; return 1 if $itemDetails && $itemDetails->{content_type}; - + my $type = Slim::Music::Info::typeFromPath($url) || 'nada'; - return 1 if $type eq 'dir'; + return 1 if $type eq 'dir'; } - + $url =~ s/^file/tmp/ if $volatileUrl; # if we have dbhighmem configured, use a memory cache to prevent slow lookups @@ -1973,13 +1987,13 @@ sub mediafolderQuery { require Slim::Player::Protocols::Volatile; Slim::Player::Protocols::Volatile->getMetadataFor($client, $url); } - + return $item; } }; if ( !defined $url && !defined $folderId && scalar(@$mediaDirs) > 1) { - + $items = $mediaDirs; $count = scalar(@$items); $topPath = ''; @@ -2034,14 +2048,14 @@ sub mediafolderQuery { $sql = 'SELECT * FROM videos WHERE url = ?'; } } - + # if this is a follow up query ($index > 0), try to read from the cache my $cacheKey = md5_hex(($params->{url} || $params->{id} || '') . $type . Slim::Music::VirtualLibraries->getLibraryIdForClient($client)); if (my $cachedItem = $bmfCache{$cacheKey}) { $items = $cachedItem->{items}; $topLevelObj = $cachedItem->{topLevelObj}; $count = $cachedItem->{count}; - + # bump the timeout on the cache $bmfCache{$cacheKey} = $cachedItem; } @@ -2050,9 +2064,9 @@ sub mediafolderQuery { ($topLevelObj, $files, $count) = Slim::Utils::Misc::findAndScanDirectoryTree($params); $topPath = blessed($topLevelObj) ? $topLevelObj->path : ''; - + my $sth = (!$type || $type eq 'audio') ? Slim::Schema->dbh->prepare_cached('SELECT content_type FROM tracks WHERE url = ?') : undef; - + my $chunkCount = 0; $items = [ grep { main::idleStreams() unless ++$chunkCount % 20; @@ -2062,8 +2076,8 @@ sub mediafolderQuery { $sth->finish() if $sth; $count = scalar @$items; - - # cache results in case the same folder is queried again shortly + + # cache results in case the same folder is queried again shortly # should speed up Jive BMF, as only the first chunk needs to run the full loop above $bmfCache{$cacheKey} = { items => $items, @@ -2099,7 +2113,7 @@ sub mediafolderQuery { $realName = ''; my $item = $filter->($filename, $topPath) || ''; - if ( (!blessed($item) || !$item->can('content_type')) + if ( (!blessed($item) || !$item->can('content_type')) && (!$params->{typeRegEx} || $filename !~ $params->{typeRegEx}) ) { logError("Invalid item found in pre-filtered list - this should not happen! ($topPath -> $filename)"); @@ -2112,19 +2126,19 @@ sub mediafolderQuery { $x++; main::idleStreams() unless $x % 20; - + $id += 0; my $url = $item->url; - + # if we're dealing with temporary items, store the real URL in $volatileUrl if ($volatileUrl) { $volatileUrl = $url; $volatileUrl =~ s/^tmp/file/; } - + $realName ||= Slim::Music::Info::fileName($volatileUrl || $url); - + # volatile folder in browse root? my $isDir; if (!$realName || Slim::Music::Info::isVolatileURL($realName) && $id < 0) { @@ -2135,31 +2149,31 @@ sub mediafolderQuery { } my $textKey = uc(substr($realName, 0, 1)); - + $request->addResultLoop($loopname, $chunkCount, 'id', $id); $request->addResultLoop($loopname, $chunkCount, 'filename', $realName); - + if ($isDir || Slim::Music::Info::isDir($volatileUrl || $item)) { $request->addResultLoop($loopname, $chunkCount, 'type', 'folder'); } elsif (Slim::Music::Info::isPlaylist($volatileUrl || $item)) { $request->addResultLoop($loopname, $chunkCount, 'type', 'playlist'); } elsif ($params->{typeRegEx} && $filename =~ $params->{typeRegEx}) { $request->addResultLoop($loopname, $chunkCount, 'type', $type); - + # only do this for images & videos where we'll need the hash for the artwork if ($sth) { $sth->execute($volatileUrl || $url); - + my $itemDetails = $sth->fetchrow_hashref; - + if ($type eq 'video') { foreach my $k (keys %$itemDetails) { $itemDetails->{"videos.$k"} = $itemDetails->{$k} unless $k =~ /^videos\./; } - + _videoData($request, $loopname, $chunkCount, $tags, $itemDetails); } - + elsif ($type eq 'image') { utf8::decode( $itemDetails->{'images.title'} ) if exists $itemDetails->{'images.title'}; utf8::decode( $itemDetails->{'images.album'} ) if exists $itemDetails->{'images.album'}; @@ -2169,9 +2183,9 @@ sub mediafolderQuery { } _imageData($request, $loopname, $chunkCount, $tags, $itemDetails); } - + } - + } elsif (Slim::Music::Info::isSong($volatileUrl || $item) && $type ne 'video') { $request->addResultLoop($loopname, $chunkCount, 'type', 'track'); } elsif (-d Slim::Utils::Misc::pathFromMacAlias($volatileUrl || $url)) { @@ -2186,14 +2200,19 @@ sub mediafolderQuery { $tags =~ /u/ && $request->addResultLoop($loopname, $chunkCount, 'url', $url); $tags =~ /t/ && $request->addResultLoop($loopname, $chunkCount, 'title', $realName); + # XXX - This is not in line with other queries requesting the content type, + # where the latter would be returned as the "type" value. But I don't + # want to break backwards compatibility, therefore returning 'ct' instead. + $tags =~ /o/ && $request->addResultLoop($loopname, $chunkCount, 'ct', $item->content_type); + $chunkCount++; } - + $sth->finish() if $sth; } $request->addResult('count', $count); - + if (!$volatileUrl) { # we might have changed - flush to the db to be in sync. $topLevelObj->update if blessed($topLevelObj); @@ -2203,16 +2222,16 @@ sub mediafolderQuery { Slim::Schema->wipeCaches; Slim::Music::Import->setLastScanTime(); } - + if ( $highmem ) { # don't grow infinitely - reset after 512 entries if ( scalar keys %$bmfUrlForName > 512 ) { $bmfUrlForName = {}; } - + $cache->{bmfUrlForName} = $bmfUrlForName; } - + $request->setStatusDone(); } @@ -2225,12 +2244,12 @@ sub nameQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $client = $request->client(); $request->addResult("_value", $client->name()); - + $request->setStatusDone(); } @@ -2243,27 +2262,27 @@ sub playerXQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $entity; $entity = $request->getRequest(1); # if element 1 is 'player', that means next element is the entity - $entity = $request->getRequest(2) if $entity eq 'player'; + $entity = $request->getRequest(2) if $entity eq 'player'; my $clientparam = $request->getParam('_IDorIndex'); - + if ($entity eq 'count') { $request->addResult("_$entity", Slim::Player::Client::clientCount()); - } else { + } else { my $client; - + # were we passed an ID? if (defined $clientparam && Slim::Utils::Misc::validMacAddress($clientparam)) { $client = Slim::Player::Client::getClient($clientparam); } else { - + # otherwise, try for an index my @clients = Slim::Player::Client::clients(); @@ -2298,7 +2317,7 @@ sub playerXQuery { } } } - + $request->setStatusDone(); } @@ -2310,19 +2329,19 @@ sub playersQuery { $request->setStatusBadDispatch(); return; } - + # get our parameters my $index = $request->getParam('_index'); my $quantity = $request->getParam('_quantity'); - + my @prefs; - + if (defined(my $pref_list = $request->getParam('playerprefs'))) { # split on commas @prefs = split(/,/, $pref_list); } - + my $count = Slim::Player::Client::clientCount(); $count += 0; @@ -2330,51 +2349,74 @@ sub playersQuery { $request->addResult('count', $count); if ($valid) { - my $idx = $start; - my $cnt = 0; - my @players = Slim::Player::Client::clients(); - - if (scalar(@players) > 0) { - - for my $eachclient (@players[$start..$end]) { - $request->addResultLoop('players_loop', $cnt, - 'playerindex', $idx); - $request->addResultLoop('players_loop', $cnt, - 'playerid', $eachclient->id()); - $request->addResultLoop('players_loop', $cnt, - 'uuid', $eachclient->uuid()); - $request->addResultLoop('players_loop', $cnt, - 'ip', $eachclient->ipport()); - $request->addResultLoop('players_loop', $cnt, - 'name', $eachclient->name()); - $request->addResultLoop('players_loop', $cnt, - 'model', $eachclient->model(1)); - $request->addResultLoop('players_loop', $cnt, - 'isplayer', $eachclient->isPlayer()); - $request->addResultLoop('players_loop', $cnt, - 'displaytype', $eachclient->vfdmodel()) - unless ($eachclient->model() eq 'http'); - $request->addResultLoop('players_loop', $cnt, - 'canpoweroff', $eachclient->canPowerOff()); - $request->addResultLoop('players_loop', $cnt, - 'connected', ($eachclient->connected() || 0)); - - for my $pref (@prefs) { - if (defined(my $value = $prefs->client($eachclient)->get($pref))) { - $request->addResultLoop('players_loop', $cnt, - $pref, $value); - } - } - - $idx++; - $cnt++; - } - } + _addPlayersLoop($request, $start, $end, \@prefs); } - + $request->setStatusDone(); } +sub _addPlayersLoop { + my ($request, $start, $end, $savePrefs) = @_; + + my $idx = $start; + my $cnt = 0; + my @players = Slim::Player::Client::clients(); + + if (scalar(@players) > 0) { + + for my $eachclient (@players[$start..$end]) { + $request->addResultLoop('players_loop', $cnt, + 'playerindex', $idx); + $request->addResultLoop('players_loop', $cnt, + 'playerid', $eachclient->id()); + $request->addResultLoop('players_loop', $cnt, + 'uuid', $eachclient->uuid()); + $request->addResultLoop('players_loop', $cnt, + 'ip', $eachclient->ipport()); + $request->addResultLoop('players_loop', $cnt, + 'name', $eachclient->name()); + if (defined $eachclient->sequenceNumber()) { + $request->addResultLoop('players_loop', $cnt, + 'seq_no', $eachclient->sequenceNumber()); + } + $request->addResultLoop('players_loop', $cnt, + 'model', $eachclient->model(1)); + $request->addResultLoop('players_loop', $cnt, + 'modelname', $eachclient->modelName()); + $request->addResultLoop('players_loop', $cnt, + 'power', $eachclient->power() ? 1 : 0); + $request->addResultLoop('players_loop', $cnt, + 'isplaying', $eachclient->isPlaying() ? 1 : 0); + $request->addResultLoop('players_loop', $cnt, + 'displaytype', $eachclient->vfdmodel()) + unless ($eachclient->model() eq 'http'); + $request->addResultLoop('players_loop', $cnt, + 'isplayer', $eachclient->isPlayer() || 0); + $request->addResultLoop('players_loop', $cnt, + 'canpoweroff', $eachclient->canPowerOff()); + $request->addResultLoop('players_loop', $cnt, + 'connected', ($eachclient->connected() || 0)); + $request->addResultLoop('players_loop', $cnt, + 'firmware', $eachclient->revision()); + $request->addResultLoop('players_loop', $cnt, + 'player_needs_upgrade', 1) + if ($eachclient->needsUpgrade()); + $request->addResultLoop('players_loop', $cnt, + 'player_is_upgrading', 1) + if ($eachclient->isUpgrading()); + + for my $pref (@$savePrefs) { + if (defined(my $value = $prefs->client($eachclient)->get($pref))) { + $request->addResultLoop('players_loop', $cnt, + $pref, $value); + } + } + + $idx++; + $cnt++; + } + } +} sub playlistPlaylistsinfoQuery { my $request = shift; @@ -2384,24 +2426,24 @@ sub playlistPlaylistsinfoQuery { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); my $playlistObj = $client->currentPlaylist(); - + if (blessed($playlistObj)) { if ($playlistObj->can('id')) { $request->addResult("id", $playlistObj->id()); } $request->addResult("name", $playlistObj->title()); - + $request->addResult("modified", $client->currentPlaylistModified()); $request->addResult("url", $playlistObj->url()); } - + $request->setStatusDone(); } @@ -2410,18 +2452,18 @@ sub playlistXQuery { my $request = shift; # check this is the correct query - if ($request->isNotQuery([['playlist'], ['name', 'url', 'modified', - 'tracks', 'duration', 'artist', 'album', 'title', 'genre', 'path', + if ($request->isNotQuery([['playlist'], ['name', 'url', 'modified', + 'tracks', 'duration', 'artist', 'album', 'title', 'genre', 'path', 'repeat', 'shuffle', 'index', 'jump', 'remote']])) { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); my $entity = $request->getRequest(1); my $index = $request->getParam('_index'); - + if ($entity eq 'repeat') { $request->addResult("_$entity", Slim::Player::Playlist::repeat($client)); @@ -2452,7 +2494,7 @@ sub playlistXQuery { if (defined (my $url = Slim::Player::Playlist::url($client, $index))) { $request->addResult("_$entity", Slim::Music::Info::isRemoteURL($url)); } - + } elsif ($entity =~ /(duration|artist|album|title|genre|name)/) { my $songData = _songData( @@ -2460,7 +2502,7 @@ sub playlistXQuery { Slim::Player::Playlist::song($client, $index), 'dalgN', # tags needed for our entities ); - + if (defined $songData->{$entity}) { $request->addResult("_$entity", $songData->{$entity}); } @@ -2468,7 +2510,7 @@ sub playlistXQuery { $request->addResult("_$entity", $songData->{remote_title}); } } - + $request->setStatusDone(); } @@ -2476,7 +2518,7 @@ sub playlistXQuery { # Can't use _getTagDataForTracks as is, as we have to deal with remote URLs, too sub playlistsTracksQuery { my $request = shift; - + # check this is the correct query. # "playlisttracks" is deprecated (July 06). if ($request->isNotQuery([['playlisttracks']]) && @@ -2512,7 +2554,7 @@ sub playlistsTracksQuery { } # now build the result - + if (Slim::Music::Import->stillScanning()) { $request->addResult("rescan", 1); } @@ -2521,7 +2563,7 @@ sub playlistsTracksQuery { my $count = $iterator->count(); $count += 0; - + my ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), $count); if ($valid || $start == $end) { @@ -2530,15 +2572,15 @@ sub playlistsTracksQuery { my $cur = $start; my $loopname = 'playlisttracks_loop'; my $chunkCount = 0; - + my $list_index = 0; for my $eachitem ($iterator->slice($start, $end)) { _addSong($request, $loopname, $chunkCount, $eachitem, $tags, "playlist index", $cur); - + $cur++; $chunkCount++; - + main::idleStreams(); } } @@ -2549,7 +2591,7 @@ sub playlistsTracksQuery { $request->addResult("count", 0); } - $request->setStatusDone(); + $request->setStatusDone(); } @@ -2568,7 +2610,7 @@ sub playlistsQuery { my $search = $request->getParam('search'); my $tags = $request->getParam('tags') || ''; my $libraryId= Slim::Music::VirtualLibraries->getRealId($request->getParam('library_id')); - + # Normalize any search parameters if (defined $search && !Slim::Schema->canFulltextSearch) { $search = Slim::Utils::Text::searchStringSplit($search); @@ -2578,7 +2620,7 @@ sub playlistsQuery { # now build the result my $count = $rs->count; - + if (Slim::Music::Import->stillScanning()) { $request->addResult("rescan", 1); } @@ -2586,12 +2628,12 @@ sub playlistsQuery { if (defined $rs) { $count += 0; - + my ($valid, $start, $end) = $request->normalize( scalar($index), scalar($quantity), $count); if ($valid) { - + my $loopname = 'playlists_loop'; my $chunkCount = 0; @@ -2599,7 +2641,7 @@ sub playlistsQuery { my $id = $eachitem->id(); $id += 0; - + my $textKey = substr($eachitem->namesort, 0, 1); $request->addResultLoop($loopname, $chunkCount, "id", $id); @@ -2608,7 +2650,7 @@ sub playlistsQuery { $tags =~ /s/ && $request->addResultLoop($loopname, $chunkCount, 'textkey', $textKey); $chunkCount++; - + main::idleStreams() if !($chunkCount % 5); } } @@ -2630,12 +2672,12 @@ sub powerQuery { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); $request->addResult('_power', $client->power()); - + $request->setStatusDone(); } @@ -2648,14 +2690,14 @@ sub prefQuery { $request->setStatusBadDispatch(); return; } - + my $client; if ($request->isQuery([['playerpref']])) { - + $client = $request->client(); - - unless ($client) { + + unless ($client) { $request->setStatusBadDispatch(); return; } @@ -2670,7 +2712,7 @@ sub prefQuery { $namespace = $1; $prefName = $2; } - + if (!defined $prefName || !defined $namespace) { $request->setStatusBadParams(); return; @@ -2680,7 +2722,7 @@ sub prefQuery { ? preferences($namespace)->client($client)->get($prefName) : preferences($namespace)->get($prefName) ); - + $request->setStatusDone(); } @@ -2693,7 +2735,7 @@ sub prefValidateQuery { $request->setStatusBadDispatch(); return; } - + my $client = $request->client(); # get our parameters @@ -2706,20 +2748,20 @@ sub prefValidateQuery { $namespace = $1; $prefName = $2; } - + if (!defined $prefName || !defined $namespace || !defined $newValue) { $request->setStatusBadParams(); return; } - $request->addResult('valid', + $request->addResult('valid', ($client ? preferences($namespace)->client($client)->validate($prefName, $newValue) : preferences($namespace)->validate($prefName, $newValue) - ) + ) ? 1 : 0 ); - + $request->setStatusDone(); } @@ -2742,7 +2784,7 @@ sub readDirectoryQuery { my $filter = $request->getParam('filter'); use File::Spec::Functions qw(catdir); - my @fsitems; # raw list of items + my @fsitems; # raw list of items my %fsitems; # meta data cache if (main::ISWINDOWS && $folder eq '/') { @@ -2751,7 +2793,7 @@ sub readDirectoryQuery { d => 1, f => 0 }; - "$_"; + "$_"; } Slim::Utils::OS::Win32->getDrives(); $folder = ''; } @@ -2761,10 +2803,12 @@ sub readDirectoryQuery { my $filterRE = qr/./ unless ($filter eq 'musicfiles'); # get file system items in $folder - @fsitems = Slim::Utils::Misc::readDirectory(catdir($folder), $filterRE); - map { + @fsitems = Slim::Utils::Misc::readDirectory($folder, $filterRE); + my $transformFn = main::ISWINDOWS ? sub { Slim::Utils::Unicode::encode_locale($_[0]) } : sub { $_[0] }; + map { + Slim::Utils::Unicode::utf8on($_) if !main::ISWINDOWS && Slim::Utils::Unicode::looks_like_utf8($_); $fsitems{$_} = { - d => -d catdir($folder, $_), + d => -d $transformFn->(catdir($folder, $_)), f => -f _ } } @fsitems; @@ -2801,7 +2845,7 @@ sub readDirectoryQuery { if (scalar(@fsitems)) { # sort folders < files - @fsitems = sort { + @fsitems = sort { if ($fsitems{$a}->{d}) { if ($fsitems{$b}->{d}) { uc($a) cmp uc($b) } else { -1 } @@ -2814,30 +2858,33 @@ sub readDirectoryQuery { my $path; for my $item (@fsitems[$start..$end]) { + my $name = $item; + + $item = Slim::Utils::Unicode::utf8decode_locale($item); + $path = ($folder ? catdir($folder, $item) : $item); - my $name = $item; my $decodedName; # display full name if we got a Windows 8.3 file name if (main::ISWINDOWS && $name =~ /~\d/) { - $decodedName = Slim::Music::Info::fileName($path); + $decodedName = basename(Slim::Music::Info::fileName($path)); } else { $decodedName = Slim::Utils::Unicode::utf8decode_locale($name); } $request->addResultLoop('fsitems_loop', $cnt, 'path', Slim::Utils::Unicode::utf8decode_locale($path)); $request->addResultLoop('fsitems_loop', $cnt, 'name', $decodedName); - + $request->addResultLoop('fsitems_loop', $cnt, 'isfolder', $fsitems{$item}->{d}); $idx++; $cnt++; - } + } } } - $request->setStatusDone(); + $request->setStatusDone(); } @@ -2853,7 +2900,7 @@ sub rescanQuery { # no params for the rescan query $request->addResult('_rescan', Slim::Music::Import->stillScanning() ? 1 : 0); - + $request->setStatusDone(); } @@ -2885,7 +2932,7 @@ sub rescanprogressQuery { my @steps; for my $p (@progress) { - + my $name = $p->name; if ($name =~ /(.*)\|(.*)/) { $request->addResult('fullname', $request->string($2 . '_PROGRESS') . $request->string('COLON') . ' ' . $1); @@ -2894,18 +2941,18 @@ sub rescanprogressQuery { my $percComplete = $p->finish ? 100 : $p->total ? $p->done / $p->total * 100 : -1; $request->addResult($name, int($percComplete)); - + push @steps, $name; $total_time += ($p->finish || time()) - $p->start; - + if ($p->active && $p->info) { $request->addResult('info', $p->info); } } - + $request->addResult('steps', join(',', @steps)) if @steps; # report it @@ -2913,7 +2960,7 @@ sub rescanprogressQuery { my $mins = int(($total_time - $hrs * 3600)/60); my $sec = $total_time - (3600 * $hrs) - (60 * $mins); $request->addResult('totaltime', sprintf("%02d:%02d:%02d", $hrs, $mins, $sec)); - + # if we're not scanning, just say so... } else { $request->addResult('rescan', 0); @@ -2961,20 +3008,20 @@ sub searchQuery { my $totalCount = 0; my $search = Slim::Schema->canFulltextSearch ? $query : Slim::Utils::Text::searchStringSplit($query); - + my $dbh = Slim::Schema->dbh; - + my $total = 0; - + my $doSearch = sub { my ($type, $name, $w, $p, $c) = @_; - + # contributors first my $cols = "me.id, me.$name"; $cols = join(', ', $cols, @$c) if $extended && $c && @$c; - + my $sql; - + # we don't have a full text index for genres my $canFulltextSearch = $type ne 'genre' && Slim::Schema->canFulltextSearch; @@ -2985,17 +3032,17 @@ sub searchQuery { type => $type, checkLargeResultset => sub { my $isLarge = shift; - return ($isLarge && $isLarge > ($index + $quantity)) ? ('LIMIT ' . $isLarge) : ''; + return ($isLarge && $isLarge > ($index + $quantity)) ? ('ORDER BY fulltextweight DESC LIMIT ' . $isLarge) : ''; }, }); - + $sql = "SELECT $cols, quickSearch.fulltextweight FROM quickSearch, ${type}s me "; unshift @{$w}, "me.id = quickSearch.id"; } else { $sql = "SELECT $cols FROM ${type}s me "; } - + if ( $libraryID ) { if ( $type eq 'contributor') { $sql .= 'JOIN contributor_track ON contributor_track.contributor = me.id '; @@ -3012,14 +3059,14 @@ sub searchQuery { elsif ( $type eq 'track' ) { $sql .= 'JOIN library_track ON library_track.track = me.id '; } - + push @{$w}, 'library_track.library = ?'; push @{$p}, $libraryID; } - + if ( !$canFulltextSearch ) { my $s = ref $search ? $search : [ $search ]; - + if ( ref $s->[0] eq 'ARRAY' ) { push @{$w}, '(' . join( ' OR ', map { "me.${name}search LIKE ?" } @{ $s->[0] } ) . ')'; push @{$p}, @{ $s->[0] }; @@ -3029,59 +3076,59 @@ sub searchQuery { push @{$p}, @{$s}; } } - + if ( $w && @{$w} ) { $sql .= 'WHERE '; my $s = join( ' AND ', @{$w} ); $s =~ s/\%/\%\%/g; $sql .= $s . ' '; } - + $sql .= "GROUP BY me.id " if $libraryID; my $sth = $dbh->prepare_cached( qq{SELECT COUNT(1) FROM ($sql) AS t1} ); $sth->execute(@$p); my ($count) = $sth->fetchrow_array; $sth->finish; - + $count += 0; $total += $count; - + my ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), $count); - + if ($valid) { $request->addResult("${type}s_count", $count); $sql .= "ORDER BY quickSearch.fulltextweight DESC " if $canFulltextSearch; - + # Limit the real query $sql .= "LIMIT ?,?"; - + my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p}, $index, $quantity ); - + my ($id, $title, %additionalCols); $sth->bind_col(1, \$id); $sth->bind_col(2, \$title); - + if ($extended && $c) { my $i = 2; foreach (@$c) { $sth->bind_col(++$i, \$additionalCols{$_}); } } - + my $chunkCount = 0; my $loopname = "${type}s_loop"; while ( $sth->fetch ) { - + last if $chunkCount >= $quantity; - + $request->addResultLoop($loopname, $chunkCount, "${type}_id", $id+0); utf8::decode($title); $request->addResultLoop($loopname, $chunkCount, "${type}", $title); - + # any additional column if ($extended && $c) { foreach (@$c) { @@ -3094,12 +3141,12 @@ sub searchQuery { $request->addResultLoop($loopname, $chunkCount, $col, $value); } } - + $chunkCount++; - + main::idleStreams() if !($chunkCount % 10); } - + $sth->finish; } }; @@ -3108,9 +3155,9 @@ sub searchQuery { $doSearch->('album', 'title', undef, undef, ['me.artwork']); $doSearch->('genre', 'name'); $doSearch->('track', 'title', ['me.audio = ?'], ['1'], ['me.coverid', 'me.audio']); - + # XXX - should we search for playlists, too? - + $request->addResult('count', $total); $request->setStatusDone(); } @@ -3121,18 +3168,18 @@ sub searchQuery { sub serverstatusQuery_filter { my $self = shift; my $request = shift; - + # we want to know about clients going away as soon as possible if ($request->isCommand([['client'], ['forget']]) || $request->isCommand([['connect']])) { return 1; } - + # we want to know about rescan and all client notifs, as well as power on/off # FIXME: wipecache and rescan are synonyms... if ($request->isCommand([['wipecache', 'rescan', 'client', 'power']])) { return 1.3; } - + # FIXME: prefset??? # we want to know about any pref in our array if (defined(my $prefsPtr = $self->privateData()->{'server'})) { @@ -3156,14 +3203,14 @@ sub serverstatusQuery_filter { if ($request->isCommand([['name']])) { return 1.3; } - + return 0; } sub serverstatusQuery { my $request = shift; - + main::INFOLOG && $log->debug("serverstatusQuery()"); # check this is the correct query @@ -3171,7 +3218,7 @@ sub serverstatusQuery { $request->setStatusBadDispatch(); return; } - + if (Slim::Schema::hasLibrary()) { if (Slim::Music::Import->stillScanning()) { $request->addResult('rescan', "1"); @@ -3180,7 +3227,7 @@ sub serverstatusQuery { # remove leading path information from the progress name my $name = $p->name; $name =~ s/(.*)\|//; - + $request->addResult('progressname', $request->string($name . '_PROGRESS')); $request->addResult('progressdone', $p->done); $request->addResult('progresstotal', $p->total); @@ -3195,17 +3242,24 @@ sub serverstatusQuery { #} } } - + # add version $request->addResult('version', $::VERSION); # add server_uuid $request->addResult('uuid', $prefs->get('server_uuid')); + if ( my $mac = Slim::Utils::OSDetect->getOS()->getMACAddress() ) { + $request->addResult('mac', $mac); + } + + $request->addResult('ip', Slim::Utils::Network::serverAddr()); + $request->addResult('httpport', $prefs->get('httpport')); + if (Slim::Schema::hasLibrary()) { # add totals my $totals = Slim::Schema->totals($request->client); - + $request->addResult("info total albums", $totals->{album}); $request->addResult("info total artists", $totals->{contributor}); $request->addResult("info total genres", $totals->{genre}); @@ -3219,7 +3273,7 @@ sub serverstatusQuery { # split on commas my @prefs = split(/,/, $pref_list); $savePrefs{'server'} = \@prefs; - + for my $pref (@{$savePrefs{'server'}}) { if (defined(my $value = $prefs->get($pref))) { $request->addResult($pref, $value); @@ -3231,7 +3285,7 @@ sub serverstatusQuery { # split on commas my @prefs = split(/,/, $pref_list); $savePrefs{'player'} = \@prefs; - + } @@ -3247,91 +3301,40 @@ sub serverstatusQuery { my ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), $count); if ($valid) { - - my $cnt = 0; - my @players = Slim::Player::Client::clients(); - - if (scalar(@players) > 0) { - - for my $eachclient (@players[$start..$end]) { - $request->addResultLoop('players_loop', $cnt, - 'playerid', $eachclient->id()); - $request->addResultLoop('players_loop', $cnt, - 'uuid', $eachclient->uuid()); - $request->addResultLoop('players_loop', $cnt, - 'ip', $eachclient->ipport()); - $request->addResultLoop('players_loop', $cnt, - 'name', $eachclient->name()); - if (defined $eachclient->sequenceNumber()) { - $request->addResultLoop('players_loop', $cnt, - 'seq_no', $eachclient->sequenceNumber()); - } - $request->addResultLoop('players_loop', $cnt, - 'model', $eachclient->model(1)); - $request->addResultLoop('players_loop', $cnt, - 'power', $eachclient->power()); - $request->addResultLoop('players_loop', $cnt, - 'isplaying', $eachclient->isPlaying() ? 1 : 0); - $request->addResultLoop('players_loop', $cnt, - 'displaytype', $eachclient->vfdmodel()) - unless ($eachclient->model() eq 'http'); - $request->addResultLoop('players_loop', $cnt, - 'canpoweroff', $eachclient->canPowerOff()); - $request->addResultLoop('players_loop', $cnt, - 'connected', ($eachclient->connected() || 0)); - $request->addResultLoop('players_loop', $cnt, - 'isplayer', ($eachclient->isPlayer() || 0)); - $request->addResultLoop('players_loop', $cnt, - 'player_needs_upgrade', "1") - if ($eachclient->needsUpgrade()); - $request->addResultLoop('players_loop', $cnt, - 'player_is_upgrading', "1") - if ($eachclient->isUpgrading()); - - for my $pref (@{$savePrefs{'player'}}) { - if (defined(my $value = $prefs->client($eachclient)->get($pref))) { - $request->addResultLoop('players_loop', $cnt, - $pref, $value); - } - } - - $cnt++; - } - } - + _addPlayersLoop($request, $start, $end, $savePrefs{'player'}); } if (!main::NOMYSB) { # return list of players connected to SN my @sn_players = Slim::Networking::SqueezeNetwork::Players->get_players(); - + $count = scalar @sn_players || 0; - + $request->addResult('sn player count', $count); - + ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), $count); - + if ($valid) { - + my $sn_cnt = 0; - + for my $player ( @sn_players ) { $request->addResultLoop( 'sn_players_loop', $sn_cnt, 'id', $player->{id} ); - - $request->addResultLoop( + + $request->addResultLoop( 'sn_players_loop', $sn_cnt, 'name', $player->{name} ); - + $request->addResultLoop( 'sn_players_loop', $sn_cnt, 'playerid', $player->{mac} ); - + $request->addResultLoop( 'sn_players_loop', $sn_cnt, 'model', $player->{model} ); - + $sn_cnt++; } } @@ -3349,13 +3352,13 @@ sub serverstatusQuery { if ($valid) { my $other_cnt = 0; - + for my $player ( keys %{$other_players} ) { $request->addResultLoop( 'other_players_loop', $other_cnt, 'playerid', $player ); - $request->addResultLoop( + $request->addResultLoop( 'other_players_loop', $other_cnt, 'name', $other_players->{$player}->{name} ); @@ -3368,24 +3371,24 @@ sub serverstatusQuery { ); $request->addResultLoop( - 'other_players_loop', $other_cnt, 'serverurl', + 'other_players_loop', $other_cnt, 'serverurl', Slim::Networking::Discovery::Server::getWebHostAddress($other_players->{$player}->{server}) ); $other_cnt++; } } - + # manage the subscription if (defined(my $timeout = $request->getParam('subscribe'))) { - + # store the prefs array as private data so our filter above can find it back $request->privateData(\%savePrefs); - + # register ourselves to be automatically re-executed on timeout or filter $request->registerAutoExecute($timeout, \&serverstatusQuery_filter); } - + $request->setStatusDone(); } @@ -3398,12 +3401,12 @@ sub signalstrengthQuery { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); $request->addResult('_signalstrength', $client->signalStrength() || 0); - + $request->setStatusDone(); } @@ -3416,7 +3419,7 @@ sub sleepQuery { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); @@ -3424,9 +3427,9 @@ sub sleepQuery { if ($isValue < 0) { $isValue = 0; } - + $request->addResult('_sleep', $isValue); - + $request->setStatusDone(); } @@ -3436,18 +3439,18 @@ sub sleepQuery { sub statusQuery_filter { my $self = shift; my $request = shift; - + # retrieve the clientid, abort if not about us my $clientid = $request->clientid() || return 0; my $myclientid = $self->clientid() || return 0; - + # Bug 10064: playlist notifications get sent to everyone in the sync-group if ($request->isCommand([['playlist', 'newmetadata']]) && (my $client = $request->client)) { return 0 if !grep($_->id eq $myclientid, $client->syncGroupActiveMembers()); } else { return 0 if $clientid ne $myclientid; } - + # ignore most prefset commands, but e.g. alarmSnoozeSeconds needs to generate a playerstatus update if ( $request->isCommand( [['prefset', 'playerpref']] ) ) { my $prefname = $request->getParam('_prefname'); @@ -3464,11 +3467,11 @@ sub statusQuery_filter { # special case: the client is gone! if ($request->isCommand([['client'], ['forget']])) { - + # pretend we do not need a client, otherwise execute() fails # and validate() deletes the client info! $self->needClient(0); - + # we'll unsubscribe above if there is no client return 1; } @@ -3509,9 +3512,9 @@ sub statusQuery_filter { sub statusQuery { my $request = shift; - + my $isDebug = main::DEBUGLOG && $log->is_debug; - + main::DEBUGLOG && $isDebug && $log->debug("statusQuery()"); # check this is the correct query @@ -3519,11 +3522,11 @@ sub statusQuery { $request->setStatusBadDispatch(); return; } - + # get the initial parameters my $client = $request->client(); my $menu = $request->getParam('menu'); - + # menu/jive mgmt my $menuMode = defined $menu; my $useContextMenu = $request->getParam('useContextMenu'); @@ -3536,7 +3539,7 @@ sub statusQuery { # that it is still valid. goto do_it_again; } - + my $connected = $client->connected() || 0; my $power = $client->power(); my $repeat = Slim::Player::Playlist::repeat($client); @@ -3555,18 +3558,18 @@ sub statusQuery { if ($client->needsUpgrade()) { $request->addResult('player_needs_upgrade', "1"); } - + if ($client->isUpgrading()) { $request->addResult('player_is_upgrading', "1"); } - + # add player info... if (my $name = $client->name()) { $request->addResult("player_name", $name); } $request->addResult("player_connected", $connected); $request->addResult("player_ip", $client->ipport()) if $connected; - + if (my $library_id = Slim::Music::VirtualLibraries->getLibraryIdForClient($client)) { $request->addResult("library_id", $library_id); $request->addResult("library_name", Slim::Music::VirtualLibraries->getNameForId($library_id, $client)); @@ -3583,50 +3586,50 @@ sub statusQuery { $power += 0; $request->addResult("power", $power); } - + if ($client->isa('Slim::Player::Squeezebox')) { $request->addResult("signalstrength", ($client->signalStrength() || 0)); } - + my $playlist_cur_index; - + $request->addResult('mode', Slim::Player::Source::playmode($client)); if ($client->isPlaying() && !$client->isPlaying('really')) { - $request->addResult('waitingToPlay', 1); + $request->addResult('waitingToPlay', 1); } if (my $song = $client->playingSong()) { if ($song->isRemote()) { $request->addResult('remote', 1); - $request->addResult('current_title', + $request->addResult('current_title', Slim::Music::Info::getCurrentTitle($client, $song->currentTrack()->url)); } - - $request->addResult('time', + + $request->addResult('time', Slim::Player::Source::songTime($client)); # This is just here for backward compatibility with older SBC firmware $request->addResult('rate', 1); - + if (my $dur = $song->duration()) { $dur += 0; $request->addResult('duration', $dur); } - + my $canSeek = Slim::Music::Info::canSeek($client, $song); if ($canSeek) { $request->addResult('can_seek', 1); } } - + if ($client->currentSleepTime()) { my $sleep = $client->sleepTime() - Time::HiRes::time(); $request->addResult('sleep', $client->currentSleepTime() * 60); $request->addResult('will_sleep_in', ($sleep < 0 ? 0 : $sleep)); } - + if ($client->isSynced()) { my $master = $client->master(); @@ -3638,14 +3641,14 @@ sub statusQuery { $request->addResult('sync_slaves', join(",", @sync_slaves)); } - + if ($client->hasVolumeControl()) { # undefined for remote streams my $vol = $prefs->client($client)->get('volume'); $vol += 0; $request->addResult("mixer volume", $vol); } - + if ($client->maxBass() - $client->minBass() > 0) { $request->addResult("mixer bass", $client->bass()); } @@ -3661,7 +3664,7 @@ sub statusQuery { $repeat += 0; $request->addResult("playlist repeat", $repeat); $shuffle += 0; - $request->addResult("playlist shuffle", $shuffle); + $request->addResult("playlist shuffle", $shuffle); # Backwards compatibility - now obsolete $request->addResult("playlist mode", 'off'); @@ -3679,7 +3682,7 @@ sub statusQuery { if ($songCount > 0) { $playlist_cur_index = Slim::Player::Source::playingSongIndex($client); $request->addResult( - "playlist_cur_index", + "playlist_cur_index", $playlist_cur_index ); $request->addResult("playlist_timestamp", $client->currentPlaylistUpdateTime()); @@ -3692,7 +3695,7 @@ sub statusQuery { if ( defined($digitalVolumeControl) ) { $request->addResult('digital_volume_control', $digitalVolumeControl + 0); } - + # give a count in menu mode no matter what if ($menuMode) { # send information about the alarm state to SP @@ -3755,7 +3758,7 @@ sub statusQuery { my $presetData; # send detailed preset data in a separate loop so we don't break backwards compatibility for my $i (0..9) { if ( ref($presets) eq 'ARRAY' && defined $presets->[$i] ) { - if ( ref($presets->[$i]) eq 'HASH') { + if ( ref($presets->[$i]) eq 'HASH') { $presetLoop->[$i] = 1; for my $key (keys %{$presets->[$i]}) { if (defined $presets->[$i]->{$key}) { @@ -3778,9 +3781,9 @@ sub statusQuery { $songCount += 0; # add two for playlist save/clear to the count if the playlist is non-empty my $menuCount = $songCount?$songCount+2:0; - + $request->addResult("count", $menuCount); - + my $base; if ( $useContextMenu ) { # context menu for 'more' action @@ -3793,7 +3796,7 @@ sub statusQuery { go => { cmd => ['trackinfo', 'items'], params => { - menu => 'nowhere', + menu => 'nowhere', useContextMenu => 1, context => 'playlist', }, @@ -3804,18 +3807,18 @@ sub statusQuery { } $request->addResult('base', $base); } - + if ($songCount > 0) { - + main::DEBUGLOG && $isDebug && $log->debug("statusQuery(): setup non-zero player response"); # get the other parameters - my $tags = $request->getParam('tags'); + my $tags = $request->getParam('tags') || ''; my $index = $request->getParam('_index'); my $quantity = $request->getParam('_quantity'); - + my $loop = $menuMode ? 'item_loop' : 'playlist_loop'; my $totalOnly; - + if ( $menuMode ) { # Set required tags for menuMode $tags = 'aAlKNcxJ'; @@ -3838,16 +3841,16 @@ sub statusQuery { if (defined($index) && ($index eq "-")) { $modecurrent = 1; } - + # bug 9132: rating might have changed # we need to be sure we have the latest data from the DB if ratings are requested my $refreshTrack = $tags =~ /R/; - + my $track; - + if (!$totalOnly) { $track = Slim::Player::Playlist::song($client, $playlist_cur_index, $refreshTrack); - + if ($track->remote) { $tags .= "B" unless $totalOnly; # include button remapping my $metadata = _songData($request, $track, $tags); @@ -3864,16 +3867,16 @@ sub statusQuery { _addJiveSong($request, $loop, 0, $playlist_cur_index, $track); } else { - _addSong($request, $loop, 0, + _addSong($request, $loop, 0, $track, $tags, 'playlist index', $playlist_cur_index ); } - + } else { my ($valid, $start, $end); - + if ($modecurrent) { ($valid, $start, $end) = $request->normalize($playlist_cur_index, scalar($quantity), $songCount); } else { @@ -3884,11 +3887,11 @@ sub statusQuery { my $count = 0; $start += 0; $request->addResult('offset', $request->getParam('_index')) if $menuMode; - + my (@tracks, @trackIds); foreach my $track ( Slim::Player::Playlist::songs($client, $start, $end) ) { next unless defined $track; - + if ( $track->remote ) { push @tracks, $track; } @@ -3897,19 +3900,19 @@ sub statusQuery { push @trackIds, $tracks[-1]; } } - + # get hash of tagged data for all tracks my $songData = _getTagDataForTracks( $tags, { trackIds => \@trackIds, } ) if scalar @trackIds; - + # no need to use Tie::IxHash to preserve order when we return JSON Data my $fast = ($totalOnly || ($request->source && $request->source =~ m{/slim/request\b|JSONRPC|internal})) ? 1 : 0; # Slice and map playlist to get only the requested IDs $idx = $start; my $totalDuration = 0; - + foreach( @tracks ) { # Use songData for track, if remote use the object directly my $data = ref $_ ? $_ : $songData->{$_}; @@ -3930,7 +3933,7 @@ sub statusQuery { } } else { - _addSong( $request, $loop, $count, + _addSong( $request, $loop, $count, $data, $tags, 'playlist index', $idx, $fast ); @@ -3938,38 +3941,38 @@ sub statusQuery { $count++; $idx++; - + # give peace a chance... # This is need much less now that the DB query is done ahead of time main::idleStreams() if ! ($count % 20); } - + if ($totalOnly) { $request->addResult('playlist duration', $totalDuration || 0); } - + # we don't do that in menu mode! if (!$menuMode && !$totalOnly) { - + my $repShuffle = $prefs->get('reshuffleOnRepeat'); my $canPredictFuture = ($repeat == 2) # we're repeating all && # and ( ($shuffle == 0) # either we're not shuffling || # or (!$repShuffle)); # we don't reshuffle - + if ($modecurrent && $canPredictFuture && ($count < scalar($quantity))) { - + # XXX: port this to use _getTagDataForTracks # wrap around the playlist... - ($valid, $start, $end) = $request->normalize(0, (scalar($quantity) - $count), $songCount); + ($valid, $start, $end) = $request->normalize(0, (scalar($quantity) - $count), $songCount); if ($valid) { for ($idx = $start; $idx <= $end; $idx++){ - _addSong($request, $loop, $count, + _addSong($request, $loop, $count, Slim::Player::Playlist::song($client, $idx, $refreshTrack), $tags, 'playlist index', $idx ); @@ -3989,11 +3992,11 @@ do_it_again: # manage the subscription if (defined(my $timeout = $request->getParam('subscribe'))) { main::DEBUGLOG && $isDebug && $log->debug("statusQuery(): setting up subscription"); - + # register ourselves to be automatically re-executed on timeout or filter $request->registerAutoExecute($timeout, \&statusQuery_filter); } - + $request->setStatusDone(); } @@ -4015,7 +4018,7 @@ sub songinfoQuery { my $url = $request->getParam('url'); my $trackID = $request->getParam('track_id'); my $tagsprm = $request->getParam('tags'); - + if (!defined $trackID && !defined $url) { $request->setStatusBadParams(); return; @@ -4036,9 +4039,9 @@ sub songinfoQuery { $track = Slim::Schema->objectForUrl($url); } } - + # now build the result - + if (Slim::Music::Import->stillScanning()) { $request->addResult("rescan", 1); } @@ -4062,13 +4065,13 @@ sub songinfoQuery { # this is where we construct the nowplaying menu my $idx = 0; - + while (my ($key, $val) = each %{$hashRef}) { if ($idx >= $start && $idx <= $end) { - + $request->addResultLoop($loopname, $chunkCount, $key, $val); - - $chunkCount++; + + $chunkCount++; } $idx++; } @@ -4087,20 +4090,20 @@ sub syncQuery { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); if ($client->isSynced()) { - + my @sync_buddies = map { $_->id() } $client->syncedWith(); $request->addResult('_sync', join(",", @sync_buddies)); } else { - + $request->addResult('_sync', '-'); } - + $request->setStatusDone(); } @@ -4113,29 +4116,29 @@ sub syncGroupsQuery { $request->setStatusBadDispatch(); return; } - - + + my $cnt = 0; my @players = Slim::Player::Client::clients(); - my $loopname = 'syncgroups_loop'; + my $loopname = 'syncgroups_loop'; if (scalar(@players) > 0) { for my $eachclient (@players) { - + # create a group if $eachclient is a master if ($eachclient->isSynced() && Slim::Player::Sync::isMaster($eachclient)) { my @sync_buddies = map { $_->id() } $eachclient->syncedWith(); my @sync_names = map { $_->name() } $eachclient->syncedWith(); - - $request->addResultLoop($loopname, $cnt, 'sync_members', join(",", $eachclient->id, @sync_buddies)); - $request->addResultLoop($loopname, $cnt, 'sync_member_names', join(",", $eachclient->name, @sync_names)); - + + $request->addResultLoop($loopname, $cnt, 'sync_members', join(",", $eachclient->id, @sync_buddies)); + $request->addResultLoop($loopname, $cnt, 'sync_member_names', join(",", $eachclient->name, @sync_names)); + $cnt++; } } } - + $request->setStatusDone(); } @@ -4148,12 +4151,12 @@ sub timeQuery { $request->setStatusBadDispatch(); return; } - + # get the parameters my $client = $request->client(); $request->addResult('_time', Slim::Player::Source::songTime($client)); - + $request->setStatusDone(); } @@ -4167,7 +4170,7 @@ sub titlesQuery { $request->setStatusBadDispatch(); return; } - + my $tags = 'gald'; # get our parameters @@ -4187,15 +4190,15 @@ sub titlesQuery { # did we have override on the defaults? - # note that this is not equivalent to + # note that this is not equivalent to # $val = $param || $default; # since when $default eq '' -> $val eq $param $tags = $tagsprm if defined $tagsprm; - + my $collate = Slim::Utils::OSDetect->getOS()->sqlHelperClass()->collate(); my $where = '(tracks.content_type != "cpl" AND tracks.content_type != "src" AND tracks.content_type != "ssp" AND tracks.content_type != "dir")'; my $order_by = "tracks.titlesort $collate"; - + if ($sort) { if ($sort eq 'tracknum') { $tags .= 't'; @@ -4210,16 +4213,16 @@ sub titlesQuery { $order_by = "albums.titlesort, tracks.disc, tracks.tracknum, tracks.titlesort $collate"; # XXX titlesort had prepended 0 } } - + $tags .= 'R' if $search && $search =~ /tracks_persistent\.rating/ && $tags !~ /R/; $tags .= 'O' if $search && $search =~ /tracks_persistent\.playcount/ && $tags !~ /O/; - + my $stillScanning = Slim::Music::Import->stillScanning(); - + my $count; my $start; my $end; - + my ($items, $itemOrder, $totalCount) = _getTagDataForTracks( $tags, { where => $where, sort => $order_by, @@ -4233,11 +4236,11 @@ sub titlesQuery { libraryId => $libraryID, limit => sub { $count = shift; - + my $valid; ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), $count); - + return ($valid, $index, $quantity); }, } ); @@ -4254,12 +4257,12 @@ sub titlesQuery { my $chunkCount = 0; if ( scalar @{$itemOrder} ) { - + for my $trackId ( @{$itemOrder} ) { my $item = $items->{$trackId}; - + _addSong($request, $loopname, $chunkCount, $item, $tags); - + $chunkCount++; } @@ -4283,7 +4286,7 @@ sub versionQuery { # no params for the version query $request->addResult('_version', $::VERSION); - + $request->setStatusDone(); } @@ -4301,22 +4304,22 @@ sub yearsQuery { $request->setStatusNotDispatchable(); return; } - + my $sqllog = main::DEBUGLOG && logger('database.sql'); - + # get our parameters my $client = $request->client(); my $index = $request->getParam('_index'); - my $quantity = $request->getParam('_quantity'); + my $quantity = $request->getParam('_quantity'); my $year = $request->getParam('year'); my $libraryID = Slim::Music::VirtualLibraries->getRealId($request->getParam('library_id')); my $hasAlbums = $request->getParam('hasAlbums'); - + # get them all by default my $where = {}; - + my ($key, $table) = ($hasAlbums || $libraryID) ? ('albums.year', 'albums') : ('id', 'years'); - + my $sql = "SELECT DISTINCT $key FROM $table "; my $w = ["$key != '0'"]; my $p = []; @@ -4340,25 +4343,25 @@ sub yearsQuery { } my $dbh = Slim::Schema->dbh; - + # Get count of all results, the count is cached until the next rescan done event my $cacheKey = md5_hex($sql . join( '', @{$p} ) . Slim::Music::VirtualLibraries->getLibraryIdForClient($client)); - + my $count = $cache->{$cacheKey}; if ( !$count ) { my $total_sth = $dbh->prepare_cached( qq{ SELECT COUNT(1) FROM ( $sql ) AS t1 } ); - + $total_sth->execute( @{$p} ); ($count) = $total_sth->fetchrow_array(); $total_sth->finish; } - + $sql .= "ORDER BY $key DESC"; # now build the result - + if (Slim::Music::Import->stillScanning()) { $request->addResult('rescan', 1); } @@ -4381,13 +4384,13 @@ sub yearsQuery { if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Years query: $sql / " . Data::Dump::dump($p) ); } - + my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p} ); - + my $id; $sth->bind_columns(\$id); - + while ( $sth->fetch ) { $id += 0; @@ -4412,32 +4415,32 @@ sub yearsQuery { plugins. In particular, this is used to implement the CLI radios query, that returns all enabled radios plugins. This function is best understood by looking as well in the code used in the plugins. - + Each plugins does in initPlugin (edited for clarity): - + $funcptr = addDispatch(['radios'], [0, 1, 1, \&cli_radiosQuery]); - + For the first plugin, $funcptr will be undef. For all the subsequent ones $funcptr will point to the preceding plugin cli_radiosQuery() function. - + The cli_radiosQuery function looks like: - + sub cli_radiosQuery { my $request = shift; - + my $data = { #... }; - + dynamicAutoQuery($request, 'radios', $funcptr, $data); } - + The plugin only defines a hash with its own data and calls dynamicAutoQuery. - + dynamicAutoQuery will call each plugin function recursively and add the data to the request results. It checks $funcptr for undefined to know if more plugins are to be called or not. - + =cut sub dynamicAutoQuery { @@ -4465,24 +4468,24 @@ sub dynamicAutoQuery { # loops in the same request results. my $loop = $menuMode?'item_loop':$query . 's_loop'; - # if the caller asked for results in the query ("radios 0 0" returns + # if the caller asked for results in the query ("radios 0 0" returns # immediately) if ($quantity) { # add the data to the results my $cnt = $request->getResultLoopCount($loop) || 0; - + if ( ref $data eq 'HASH' && scalar keys %{$data} ) { $data->{weight} = $data->{weight} || 1000; $request->setResultLoopHash($loop, $cnt, $data); } - + # more to jump to? # note we carefully check $funcptr is not a lemon if (defined $funcptr && ref($funcptr) eq 'CODE') { - + eval { &{$funcptr}($request) }; - + # arrange for some useful logging if we fail if ($@) { @@ -4492,22 +4495,22 @@ sub dynamicAutoQuery { } } - + # $funcptr is undefined, we have everybody, now slice & count else { - + # sort if requested to do so if ($sort) { $request->sortResultLoop($loop, $sort); } - + # slice as needed my $count = $request->getResultLoopCount($loop); $request->sliceResultLoop($loop, $index, $quantity); $request->addResult('offset', $request->getParam('_index')) if $menuMode; $count += 0; $request->setResultFirst('count', $count); - + # don't forget to call that to trigger notifications, if any $request->setStatusDone(); } @@ -4528,12 +4531,12 @@ sub _addSong { my $pathOrObj = shift; # song path or object, or hash from titlesQuery my $tags = shift; # tags to use my $prefixKey = shift; # prefix key, if any - my $prefixVal = shift; # prefix value, if any + my $prefixVal = shift; # prefix value, if any my $fast = shift; - - # get the hash with the data + + # get the hash with the data my $hashRef = _songData($request, $pathOrObj, $tags, $fast); - + # add the prefix in the first position, use a fancy feature of # Tie::LLHash if (defined $prefixKey && defined $hashRef) { @@ -4544,7 +4547,7 @@ sub _addSong { (tied %{$hashRef})->Unshift($prefixKey => $prefixVal); } } - + # add it directly to the result loop $request->setResultLoopHash($loop, $index, $hashRef); } @@ -4552,9 +4555,9 @@ sub _addSong { sub _addJivePlaylistControls { my ($request, $loop, $count) = @_; - + my $client = $request->client || return; - + # clear playlist my $text = $client->string('CLEAR_PLAYLIST'); # add clear playlist and save playlist menu items @@ -4626,29 +4629,29 @@ sub _addJiveSong { my $count = shift; # loop index my $index = shift; # playlist index my $track = shift || return; - + my $songData = _songData( $request, $track, 'aAlKNcxJ', # tags needed for our entities ); - + my $isRemote = $songData->{remote}; - + $request->addResultLoop($loop, $count, 'trackType', $isRemote ? 'radio' : 'local'); - + my $text = $songData->{title}; my $title = $text; my $album = $songData->{album}; my $artist = $songData->{artist}; - + # Bug 15779, include other role data # XXX may want to include all contributor roles here? my (%artists, @artists); foreach ('albumartist', 'trackartist', 'artist') { - + next if !$songData->{$_}; - + foreach my $a ( split (/, /, $songData->{$_}) ) { if ( $a && !$artists{$a} ) { push @artists, $a; @@ -4657,7 +4660,7 @@ sub _addJiveSong { } } $artist = join(', ', @artists); - + if ( $isRemote && $text && $album && $artist ) { $request->addResult('current_title'); } @@ -4683,7 +4686,7 @@ sub _addJiveSong { # Bug 7443, check for a track cover before using the album cover my $iconId = $songData->{coverid} || $songData->{artwork_track_id}; - + if ( defined($songData->{artwork_url}) ) { $request->addResultLoop( $loop, $count, 'icon', proxiedImage($songData->{artwork_url}) ); } @@ -4715,7 +4718,7 @@ sub _addJiveSong { $request->addResultLoop($loop, $count, 'text', $text); my $params = { - 'track_id' => ($songData->{'id'} + 0), + 'track_id' => ($songData->{'id'} + 0), 'playlist_index' => $index, }; $request->addResultLoop($loop, $count, 'params', $params); @@ -4728,44 +4731,44 @@ my %tagMap = ( #------------------------------------------------------------------------------ 'u' => ['url', 'LOCATION', 'url'], #url 'o' => ['type', 'TYPE', 'content_type'], #content_type - #titlesort - #titlesearch + #titlesort + #titlesearch 'a' => ['artist', 'ARTIST', 'artistName'], #->contributors - 'e' => ['album_id', '', 'albumid'], #album + 'e' => ['album_id', '', 'albumid'], #album 'l' => ['album', 'ALBUM', 'albumname'], #->album.title 't' => ['tracknum', 'TRACK', 'tracknum'], #tracknum 'n' => ['modificationTime', 'MODTIME', 'modificationTime'], #timestamp 'D' => ['addedTime', 'ADDTIME', 'addedTime'], #added_time 'U' => ['lastUpdated', 'UPDTIME', 'lastUpdated'], #updated_time 'f' => ['filesize', 'FILELENGTH', 'filesize'], #filesize - #tag + #tag 'i' => ['disc', 'DISC', 'disc'], #disc 'j' => ['coverart', 'SHOW_ARTWORK', 'coverArtExists'], #cover - 'x' => ['remote', '', 'remote'], #remote - #audio - #audio_size + 'x' => ['remote', '', 'remote'], #remote + #audio + #audio_size #audio_offset 'y' => ['year', 'YEAR', 'year'], #year 'd' => ['duration', 'LENGTH', 'secs'], #secs - #vbr_scale + #vbr_scale 'r' => ['bitrate', 'BITRATE', 'prettyBitRate'], #bitrate - 'T' => ['samplerate', 'SAMPLERATE', 'samplerate'], #samplerate - 'I' => ['samplesize', 'SAMPLESIZE', 'samplesize'], #samplesize - 'H' => ['channels', 'CHANNELS', 'channels'], #channels + 'T' => ['samplerate', 'SAMPLERATE', 'samplerate'], #samplerate + 'I' => ['samplesize', 'SAMPLESIZE', 'samplesize'], #samplesize + 'H' => ['channels', 'CHANNELS', 'channels'], #channels 'F' => ['dlna_profile', 'DLNA_PROFILE', 'dlna_profile'], #dlna_profile #block_alignment - #endian + #endian 'm' => ['bpm', 'BPM', 'bpm'], #bpm 'v' => ['tagversion', 'TAGVERSION', 'tagversion'], #tagversion # 'z' => ['drm', '', 'drm'], #drm 'M' => ['musicmagic_mixable', '', 'musicmagic_mixable'], #musicmagic_mixable - #musicbrainz_id - #lastplayed - #lossless - 'w' => ['lyrics', 'LYRICS', 'lyrics'], #lyrics - 'R' => ['rating', 'RATING', 'rating'], #rating - 'O' => ['playcount', 'PLAYCOUNT', 'playcount'], #playcOunt - 'Y' => ['replay_gain', 'REPLAYGAIN', 'replay_gain'], #replay_gain + #musicbrainz_id + #lastplayed + #lossless + 'w' => ['lyrics', 'LYRICS', 'lyrics'], #lyrics + 'R' => ['rating', 'RATING', 'rating'], #rating + 'O' => ['playcount', 'PLAYCOUNT', 'playcount'], #playcOunt + 'Y' => ['replay_gain', 'REPLAYGAIN', 'replay_gain'], #replay_gain #replay_peak 'c' => ['coverid', 'COVERID', 'coverid'], # coverid @@ -4780,17 +4783,17 @@ my %tagMap = ( 's' => ['artist_id', '', 'artist', 'id'], #->contributors 'A' => ['', '', 'contributors', 'name'], #->contributors[role].name 'S' => ['_ids', '', 'contributors', 'id'], #->contributors[role].id - + 'q' => ['disccount', '', 'album', 'discc'], #->album.discc 'J' => ['artwork_track_id', 'COVERART', 'album', 'artwork'], #->album.artwork 'C' => ['compilation', 'COMPILATION', 'album', 'compilation'], #->album.compilation 'X' => ['album_replay_gain', 'ALBUMREPLAYGAIN', 'album', 'replay_gain'], #->album.replay_gain - + 'g' => ['genre', 'GENRE', 'genre', 'name'], #->genre_track->genre.name 'p' => ['genre_id', '', 'genre', 'id'], #->genre_track->genre.id 'G' => ['genres', 'GENRE', 'genres', 'name'], #->genre_track->genres.name 'P' => ['genre_ids', '', 'genres', 'id'], #->genre_track->genres.id - + 'k' => ['comment', 'COMMENT', 'comment'], #->comment_object ); @@ -4839,23 +4842,28 @@ my %colMap = ( sub _songDataFromHash { my ( $request, $res, $tags, $fast ) = @_; - + my %returnHash; - + # define an ordered hash for our results tie (%returnHash, "Tie::IxHash") unless $fast; - + $returnHash{id} = $res->{'tracks.id'}; $returnHash{title} = $res->{'tracks.title'}; - + my @contributorRoles = Slim::Schema::Contributor->contributorRoles; - + # loop so that stuff is returned in the order given... for my $tag (split (//, $tags)) { my $tagref = $tagMap{$tag} or next; - + # Special case for A/S which return multiple keys if ( $tag eq 'A' ) { + # if we don't have an explicit track artist defined, we're going to assume the track's artist was the track artist + if ( $res->{artist} && $res->{albumartist} && $res->{artist} ne $res->{albumartist}) { + $res->{trackartist} ||= $res->{artist}; + } + for my $role ( @contributorRoles ) { $role = lc $role; if ( defined $res->{$role} ) { @@ -4880,8 +4888,8 @@ sub _songDataFromHash { $returnHash{ $tagref->[0] } = $value; } } - } - + } + return \%returnHash; } @@ -4890,7 +4898,7 @@ sub _songData { my $pathOrObj = shift; # song path or object my $tags = shift; # tags to use my $fast = shift; # don't use Tie::IxHash for performance - + if ( ref $pathOrObj eq 'HASH' ) { # Hash from direct DBI query in titlesQuery return _songDataFromHash($request, $pathOrObj, $tags, $fast); @@ -4902,7 +4910,7 @@ sub _songData { if (!blessed($track) || !$track->can('id')) { logError("Called with invalid object or path: $pathOrObj!"); - + # For some reason, $pathOrObj may be an id... try that before giving up... if ($pathOrObj =~ /^\d+$/) { $track = Slim::Schema->find('Track', $pathOrObj); @@ -4914,27 +4922,27 @@ sub _songData { return; } } - + # If we have a remote track, check if a plugin can provide metadata my $remoteMeta = {}; my $isRemote = $track->remote; my $url = $track->url; - + if ( $isRemote ) { my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url); - + if ( $handler && $handler->can('getMetadataFor') ) { # Don't modify source data $remoteMeta = Storable::dclone( $handler->getMetadataFor( $request->client, $url ) ); - + $remoteMeta->{a} = $remoteMeta->{artist}; $remoteMeta->{A} = $remoteMeta->{artist}; $remoteMeta->{l} = $remoteMeta->{album}; $remoteMeta->{i} = $remoteMeta->{disc}; $remoteMeta->{K} = $remoteMeta->{cover}; - $remoteMeta->{d} = ( $remoteMeta->{duration} || 0 ) + 0; + $remoteMeta->{d} = ( $remoteMeta->{duration} || $remoteMeta->{secs} || 0 ) + 0; $remoteMeta->{Y} = $remoteMeta->{replay_gain}; $remoteMeta->{o} = $remoteMeta->{type}; $remoteMeta->{r} = $remoteMeta->{bitrate}; @@ -4943,7 +4951,7 @@ sub _songData { $remoteMeta->{t} = $remoteMeta->{tracknum}; } } - + my $parentTrack; if ( my $client = $request->client ) { # Bug 13062, songinfo may be called without a client if (my $song = $client->currentSongForUrl($url)) { @@ -4957,18 +4965,18 @@ sub _songData { } my %returnHash; - + # define an ordered hash for our results tie (%returnHash, "Tie::IxHash") unless $fast; $returnHash{'id'} = $track->id; $returnHash{'title'} = $remoteMeta->{title} || $track->title; - + # loop so that stuff is returned in the order given... for my $tag (split (//, $tags)) { - + my $tagref = $tagMap{$tag} or next; - + # special case, remote stream name if ($tag eq 'N') { if ($parentTrack) { @@ -4979,31 +4987,31 @@ sub _songData { } } } - + # special case for remote flag, since we had to evaluate it anyway # only include it if it is true elsif ($tag eq 'x' && $isRemote) { $returnHash{$tagref->[0]} = 1; } - + # special case artists (tag A and S) elsif ($tag eq 'A' || $tag eq 'S') { if ( my $meta = $remoteMeta->{$tag} ) { $returnHash{artist} = $meta; next; } - + if ( defined(my $submethod = $tagref->[3]) ) { - + my $postfix = ($tag eq 'S')?"_ids":""; - + foreach my $type (Slim::Schema::Contributor::contributorRoles()) { - + my $key = lc($type) . $postfix; my $contributors = $track->contributorsOfType($type) or next; my @values = map { $_ = $_->$submethod() } $contributors->all; my $value = join(', ', @values); - + if (defined $value && $value ne '') { # add the tag to the result @@ -5015,15 +5023,15 @@ sub _songData { # if we have a method/relationship for the tag elsif (defined(my $method = $tagref->[2])) { - + my $value; my $key = $tagref->[0]; - + # Override with remote track metadata if available if ( defined $remoteMeta->{$tag} ) { $value = $remoteMeta->{$tag}; } - + elsif ($method eq '' || !$track->can($method)) { next; } @@ -5033,7 +5041,7 @@ sub _songData { # call submethod if (defined(my $related = $track->$method)) { - + # array returned/genre if ( blessed($related) && $related->isa('Slim::Schema::ResultSet::Genre')) { $value = join(', ', map { $_ = $_->$submethod() } $related->all); @@ -5044,21 +5052,21 @@ sub _songData { } } } - + # simple track method else { $value = $track->$method(); } - + # correct values if (($tag eq 'R' || $tag eq 'x') && $value == 0) { $value = undef; } # we might need to proxy the image request to resize it elsif ($tag eq 'K' && $value) { - $value = proxiedImage($value); + $value = proxiedImage($value); } - + # if we have a value if (defined $value && $value ne '') { @@ -5093,6 +5101,9 @@ sub showArtwork { # Wipe cached data, called after a rescan sub wipeCaches { + my $bmfCacheObject = tied %bmfCache; + tie %bmfCache, 'Tie::Cache::LRU::Expires', EXPIRES => $bmfCacheObject->{EXPIRES}, ENTRIES => $bmfCacheObject->{ENTRIES}; + $cache = {}; } @@ -5159,16 +5170,16 @@ sub contextMenuQuery { $proxiedRequest->execute(); # Bug 13744, wrap async requests - if ( $proxiedRequest->isStatusProcessing ) { + if ( $proxiedRequest->isStatusProcessing ) { $proxiedRequest->callbackFunction( sub { $request->setRawResults( $_[0]->getResults ); $request->setStatusDone(); } ); - + $request->setStatusProcessing(); return; } - + # if we get here, we punt } else { $request->setStatusBadParams(); @@ -5192,8 +5203,8 @@ sub _contextMenuBase { 'menu' => $menu, }, itemsParams => 'params', - window => { - isContextMenu => 1, + window => { + isContextMenu => 1, }, }; @@ -5201,7 +5212,7 @@ sub _contextMenuBase { sub _scanFailed { my ($request, $info) = @_; - + if ($info && $info eq 'SCAN_ABORTED') { $info = $request->string($info); } @@ -5209,7 +5220,7 @@ sub _scanFailed { $info = $request->string('FAILURE_PROGRESS', $request->string($info . '_PROGRESS') || '?'); } - $request->addResult('lastscanfailed', $info || '?'); + $request->addResult('lastscanfailed', $info || '?'); } =pod @@ -5230,27 +5241,27 @@ about tracks as efficiently as possible. If valid is not true, the request is aborted. This is messy but the only way to support the use of _fixCount, etc } - + Returns arrayref of hashes. =cut sub _getTagDataForTracks { my ( $tags, $args ) = @_; - + my $sqllog = main::DEBUGLOG && logger('database.sql'); - + my $collate = Slim::Utils::OSDetect->getOS()->sqlHelperClass()->collate(); - + my $sql = 'SELECT %s FROM tracks '; my $c = { 'tracks.id' => 1, 'tracks.title' => 1 }; my $w = []; my $p = []; my $total = 0; - + if ( $args->{where} ) { push @{$w}, $args->{where}; } - + my $sort = $args->{sort}; # return count only? @@ -5259,7 +5270,14 @@ sub _getTagDataForTracks { $count_only = 1; $tags = $sort = ''; } - + + # return IDs only + my $ids_only; + if ($tags eq 'II') { + $ids_only = 1; + $tags = $sort = ''; + } + # Normalize any search parameters my $search = $args->{search}; if ( $search && specified($search) ) { @@ -5279,12 +5297,12 @@ sub _getTagDataForTracks { return $isLarge ? "LIMIT $isLarge" : 'ORDER BY fulltextweight' }, }); - + $sql = 'SELECT %s FROM tracksSearch, tracks '; unshift @{$w}, "tracks.id = tracksSearch.id"; - + if (!$count_only) { - $sort = "tracksSearch.fulltextweight DESC, $sort"; + $sort = "tracksSearch.fulltextweight DESC" . ($sort ? ", $sort" : ''); } } else { @@ -5293,18 +5311,18 @@ sub _getTagDataForTracks { unshift @{$w}, '(' . join( ' OR ', map { 'tracks.titlesearch LIKE ?' } @{ $strings->[0] } ) . ')'; unshift @{$p}, @{ $strings->[0] }; } - else { + else { unshift @{$w}, 'tracks.titlesearch LIKE ?'; unshift @{$p}, @{$strings}; } } } - + if ( my $albumId = $args->{albumId} ) { push @{$w}, 'tracks.album = ?'; push @{$p}, $albumId; } - + if ( my $trackId = $args->{trackId} ) { push @{$w}, 'tracks.id = ?'; push @{$p}, $trackId; @@ -5320,48 +5338,48 @@ sub _getTagDataForTracks { push @{$w}, 'library_track.library = ?'; push @{$p}, $libraryId; } - + # Some helper functions to setup joins with less code my $join_genre_track = sub { if ( $sql !~ /JOIN genre_track/ ) { $sql .= 'JOIN genre_track ON genre_track.track = tracks.id '; } }; - + my $join_genres = sub { $join_genre_track->(); - + if ( $sql !~ /JOIN genres/ ) { $sql .= 'JOIN genres ON genres.id = genre_track.genre '; } }; - + my $join_contributor_tracks = sub { if ( $sql !~ /JOIN contributor_track/ ) { $sql .= 'JOIN contributor_track ON contributor_track.track = tracks.id '; } }; - + my $join_contributors = sub { $join_contributor_tracks->(); - + if ( $sql !~ /JOIN contributors/ ) { $sql .= 'JOIN contributors ON contributors.id = contributor_track.contributor '; } }; - + my $join_albums = sub { if ( $sql !~ /JOIN albums/ ) { $sql .= 'JOIN albums ON albums.id = tracks.album '; } }; - + my $join_tracks_persistent = sub { - if ( main::STATISTICS ) { - $sql .= 'JOIN tracks_persistent ON tracks_persistent.urlmd5 = tracks.urlmd5 '; + if ( main::STATISTICS && $sql !~ /JOIN tracks_persistent/ ) { + $sql .= 'LEFT JOIN tracks_persistent ON tracks_persistent.urlmd5 = tracks.urlmd5 '; } }; - + if ( my $genreId = $args->{genreId} ) { $join_genre_track->(); @@ -5370,7 +5388,7 @@ sub _getTagDataForTracks { push @{$w}, 'genre_track.genre IN (' . join(', ', map {'?'} @genreIDs) . ')'; push @{$p}, @genreIDs; } - + if ( my $contributorId = $args->{contributorId} ) { # handle the case where we're asked for the VA id => return compilations if ($contributorId == Slim::Schema->variousArtistsObject->id) { @@ -5383,12 +5401,12 @@ sub _getTagDataForTracks { push @{$p}, $contributorId; } } - + if ( my $trackIds = $args->{trackIds} ) { # Filter out negative tracks (remote tracks) push @{$w}, 'tracks.id IN (' . join( ',', grep { $_ > 0 } @{$trackIds} ) . ')'; } - + # Process tags and add columns/joins as needed $tags =~ /e/ && do { $c->{'tracks.album'} = 1 }; $tags =~ /d/ && do { $c->{'tracks.secs'} = 1 }; @@ -5413,24 +5431,24 @@ sub _getTagDataForTracks { $tags =~ /x/ && do { $c->{'tracks.remote'} = 1 }; $tags =~ /c/ && do { $c->{'tracks.coverid'} = 1 }; $tags =~ /Y/ && do { $c->{'tracks.replay_gain'} = 1 }; - $tags =~ /i/ && do { $c->{'tracks.disc'} = 1 }; + $tags =~ /i/ && do { $c->{'tracks.disc'} = 1 }; $tags =~ /g/ && do { $join_genres->(); $c->{'genres.name'} = 1; - + # XXX there is a bug here if a track has multiple genres, the genre # returned will be a random genre, not sure how to solve this -Andy }; - + $tags =~ /p/ && do { $join_genres->(); $c->{'genres.id'} = 1; }; - - $tags =~ /a/ && do { + + $tags =~ /[as]/ && do { $join_contributors->(); - $c->{'contributors.name'} = 1; - + $c->{'contributors.name'} = 1 if $tags =~ /a/; + # only albums on which the contributor has a specific role? my @roles; if ($args->{roleId}) { @@ -5441,7 +5459,7 @@ sub _getTagDataForTracks { # Tag 'a' returns either ARTIST or TRACKARTIST role # Bug 16791: Need to include ALBUMARTIST too @roles = ( 'ARTIST', 'TRACKARTIST', 'ALBUMARTIST' ); - + # Loop through each pref to see if the user wants to show that contributor role. foreach (Slim::Schema::Contributor->contributorRoles) { if ($prefs->get(lc($_) . 'InArtists')) { @@ -5457,51 +5475,51 @@ sub _getTagDataForTracks { push @{$w}, '(contributors.id = tracks.primary_artist OR tracks.primary_artist IS NULL)' if $args->{trackIds}; push @{$w}, 'contributor_track.role IN (' . join(', ', map {'?'} @roles) . ')'; }; - + $tags =~ /s/ && do { $join_contributors->(); $c->{'contributors.id'} = 1; }; - + $tags =~ /l/ && do { $join_albums->(); $c->{'albums.title'} = 1; }; - + $tags =~ /q/ && do { $join_albums->(); $c->{'albums.discc'} = 1; }; - + $tags =~ /J/ && do { $join_albums->(); $c->{'albums.artwork'} = 1; }; - + $tags =~ /C/ && do { $join_albums->(); $c->{'albums.compilation'} = 1; }; - + $tags =~ /X/ && do { $join_albums->(); $c->{'albums.replay_gain'} = 1; }; - + $tags =~ /R/ && do { if ( main::STATISTICS ) { $join_tracks_persistent->(); $c->{'tracks_persistent.rating'} = 1; } }; - + $tags =~ /O/ && do { if ( main::STATISTICS ) { $join_tracks_persistent->(); $c->{'tracks_persistent.playcount'} = 1; } }; - + if ( scalar @{$w} ) { $sql .= 'WHERE '; my $s = join( ' AND ', @{$w} ); @@ -5509,23 +5527,23 @@ sub _getTagDataForTracks { $sql .= $s . ' '; } $sql .= 'GROUP BY tracks.id ' if $sql =~ /JOIN /; - + if ( $sort ) { $sql .= "ORDER BY $sort "; } - + # Add selected columns # Bug 15997, AS mapping needed for MySQL my @cols = sort keys %{$c}; $sql = sprintf $sql, join( ', ', map { $_ . " AS '" . $_ . "'" } @cols ); - + my $dbh = Slim::Schema->dbh; - + if ( $count_only || (my $limit = $args->{limit}) ) { # Let the caller worry about the limit values - my $cacheKey = md5_hex($sql . join( '', @{$p} )); - + my $cacheKey = md5_hex($sql . join( '', @{$p}, @$w ) . (Slim::Utils::Text::ignoreCase($search, 1) || '')); + # use short lived cache, as we might be dealing with changing data (eg. playcount) if ( my $cached = $bmfCache{$cacheKey} ) { $total = $cached; @@ -5534,38 +5552,38 @@ sub _getTagDataForTracks { my $total_sth = $dbh->prepare_cached( qq{ SELECT COUNT(1) FROM ( $sql ) AS t1 } ); - + if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Titles totals query: SELECT COUNT(1) FROM ($sql) / " . Data::Dump::dump($p) ); } - + $total_sth->execute( @{$p} ); ($total) = $total_sth->fetchrow_array(); $total_sth->finish; - + $bmfCache{$cacheKey} = $total; } - + my ($valid, $start, $end); ($valid, $start, $end) = $limit->($total) unless $count_only; - + if ( $count_only || !$valid ) { return wantarray ? ( {}, [], $total ) : {}; } - + # Limit the real query if ( $start =~ /^\d+$/ && defined $end && $end =~ /^\d+$/ ) { $sql .= "LIMIT $start, $end "; } } - + if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "_getTagDataForTracks query: $sql / " . Data::Dump::dump($p) ); } - + my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p} ); - + # Bind selected columns in order my $i = 1; for my $col ( @cols ) { @@ -5575,29 +5593,33 @@ sub _getTagDataForTracks { $c->{$newcol} = 1; $col = $newcol; } - + $sth->bind_col( $i++, \$c->{$col} ); } - + # Results are stored in a hash keyed by track ID, and we # also store the order the data is returned in, titlesQuery # needs this to provide correctly sorted results, and I don't # want to make %results an IxHash. my %results; my @resultOrder; - + while ( $sth->fetch ) { - utf8::decode( $c->{'tracks.title'} ) if exists $c->{'tracks.title'}; - utf8::decode( $c->{'tracks.lyrics'} ) if exists $c->{'tracks.lyrics'}; - utf8::decode( $c->{'albums.title'} ) if exists $c->{'albums.title'}; - utf8::decode( $c->{'contributors.name'} ) if exists $c->{'contributors.name'}; - utf8::decode( $c->{'genres.name'} ) if exists $c->{'genres.name'}; - utf8::decode( $c->{'comments.value'} ) if exists $c->{'comments.value'}; - - $results{ $c->{'tracks.id'} } = { map { $_ => $c->{$_} } keys %{$c} }; - push @resultOrder, $c->{'tracks.id'}; - } - + if (!$ids_only) { + utf8::decode( $c->{'tracks.title'} ) if exists $c->{'tracks.title'}; + utf8::decode( $c->{'tracks.lyrics'} ) if exists $c->{'tracks.lyrics'}; + utf8::decode( $c->{'albums.title'} ) if exists $c->{'albums.title'}; + utf8::decode( $c->{'contributors.name'} ) if exists $c->{'contributors.name'}; + utf8::decode( $c->{'genres.name'} ) if exists $c->{'genres.name'}; + utf8::decode( $c->{'comments.value'} ) if exists $c->{'comments.value'}; + } + + my $id = $c->{'tracks.id'}; + + $results{ $id } = { map { $_ => $c->{$_} } keys %{$c} }; + push @resultOrder, $id; + } + # For tag A/S we have to run 1 additional query if ( $tags =~ /[AS]/ ) { my $sql = sprintf qq{ @@ -5607,41 +5629,41 @@ sub _getTagDataForTracks { WHERE contributor_track.track IN (%s) ORDER BY contributor_track.role DESC }, join( ',', @resultOrder ); - + my $contrib_sth = $dbh->prepare($sql); - + if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Tag A/S (contributor) query: $sql" ); } - + $contrib_sth->execute; - + my %values; while ( my ($id, $name, $track, $role) = $contrib_sth->fetchrow_array ) { $values{$track} ||= {}; my $role_info = $values{$track}->{$role} ||= {}; - + # XXX: what if name has ", " in it? utf8::decode($name); $role_info->{ids} .= $role_info->{ids} ? ', ' . $id : $id; $role_info->{names} .= $role_info->{names} ? ', ' . $name : $name; } - + my $want_names = $tags =~ /A/; my $want_ids = $tags =~ /S/; - + while ( my ($id, $role) = each %values ) { my $track = $results{$id}; - + while ( my ($role_id, $role_info) = each %{$role} ) { my $role = lc( Slim::Schema::Contributor->roleToType($role_id) ); - + $track->{"${role}_ids"} = $role_info->{ids} if $want_ids; $track->{$role} = $role_info->{names} if $want_names; } } } - + # Same thing for G/P, multiple genres requires another query if ( $tags =~ /[GP]/ ) { my $sql = sprintf qq{ @@ -5651,34 +5673,34 @@ sub _getTagDataForTracks { WHERE genre_track.track IN (%s) ORDER BY genres.namesort $collate }, join( ',', @resultOrder ); - + my $genre_sth = $dbh->prepare($sql); - + if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Tag G/P (genre) query: $sql" ); } - + $genre_sth->execute; - + my %values; while ( my ($id, $name, $track) = $genre_sth->fetchrow_array ) { my $genre_info = $values{$track} ||= {}; - + utf8::decode($name); $genre_info->{ids} .= $genre_info->{ids} ? ', ' . $id : $id; $genre_info->{names} .= $genre_info->{names} ? ', ' . $name : $name; } - + my $want_names = $tags =~ /G/; my $want_ids = $tags =~ /P/; - + while ( my ($id, $genre_info) = each %values ) { my $track = $results{$id}; $track->{genre_ids} = $genre_info->{ids} if $want_ids; $track->{genres} = $genre_info->{names} if $want_names; } } - + # And same for comments if ( $tags =~ /k/ ) { my $sql = sprintf qq{ @@ -5687,20 +5709,20 @@ sub _getTagDataForTracks { WHERE track IN (%s) ORDER BY id }, join( ',', @resultOrder ); - + my $comment_sth = $dbh->prepare($sql); - + if ( main::DEBUGLOG && $sqllog->is_debug ) { $sqllog->debug( "Tag k (comment) query: $sql" ); } - + $comment_sth->execute(); - + my %values; while ( my ($track, $value) = $comment_sth->fetchrow_array ) { $values{$track} .= $values{$track} ? ' / ' . $value : $value; } - + while ( my ($id, $comment) = each %values ) { utf8::decode($comment); $results{$id}->{comment} = $comment; @@ -5714,7 +5736,7 @@ sub _getTagDataForTracks { # delete the temporary table, as it's stored in memory and can be rather large Slim::Plugin::FullTextSearch::Plugin->dropHelperTable('tracksSearch') if $search && Slim::Schema->canFulltextSearch; - + return wantarray ? ( \%results, \@resultOrder, $total ) : \%results; } @@ -5728,9 +5750,9 @@ sub videoTitlesQuery { if (main::VIDEO && main::MEDIASUPPORT) { $request->setStatusNotDispatchable(); return; } - + my $sqllog = main::DEBUGLOG && logger('database.sql'); - + # get our parameters my $index = $request->getParam('_index'); my $quantity = $request->getParam('_quantity'); @@ -5738,21 +5760,21 @@ sub videoTitlesQuery { if (main::VIDEO && main::MEDIASUPPORT) { my $search = $request->getParam('search'); my $sort = $request->getParam('sort'); my $videoHash = $request->getParam('video_id'); - + #if ($sort && $request->paramNotOneOfIfDefined($sort, ['new'])) { # $request->setStatusBadParams(); # return; #} my $collate = Slim::Utils::OSDetect->getOS()->sqlHelperClass()->collate(); - + my $sql = 'SELECT %s FROM videos '; my $c = { 'videos.hash' => 1, 'videos.titlesearch' => 1, 'videos.titlesort' => 1 }; my $w = []; my $p = []; my $order_by = "videos.titlesort $collate"; my $limit; - + # Normalize and add any search parameters if ( defined $videoHash ) { push @{$w}, 'videos.hash = ?'; @@ -5788,14 +5810,14 @@ sub videoTitlesQuery { if (main::VIDEO && main::MEDIASUPPORT) { push @{$w}, '(' . join( ' OR ', map { 'videos.titlesearch LIKE ?' } @{ $strings->[0] } ) . ')'; push @{$p}, @{ $strings->[0] }; } - else { + else { push @{$w}, 'videos.titlesearch LIKE ?'; push @{$p}, @{$strings}; } } } } - + $tags =~ /t/ && do { $c->{'videos.title'} = 1 }; $tags =~ /d/ && do { $c->{'videos.secs'} = 1 }; $tags =~ /o/ && do { $c->{'videos.mime_type'} = 1 }; @@ -5815,30 +5837,30 @@ sub videoTitlesQuery { if (main::VIDEO && main::MEDIASUPPORT) { $sql .= ' '; } $sql .= "GROUP BY videos.hash ORDER BY $order_by "; - + # Add selected columns # Bug 15997, AS mapping needed for MySQL my @cols = keys %{$c}; $sql = sprintf $sql, join( ', ', map { $_ . " AS '" . $_ . "'" } @cols ); - + my $stillScanning = Slim::Music::Import->stillScanning(); - + my $dbh = Slim::Schema->dbh; - + # Get count of all results, the count is cached until the next rescan done event - my $cacheKey = md5_hex($sql . join( '', @{$p} )); - + my $cacheKey = md5_hex($sql . join( '', @{$p} ) . (Slim::Utils::Text::ignoreCase($search, 1) || '')); + my $count = $cache->{$cacheKey}; if ( !$count ) { my $total_sth = $dbh->prepare_cached( qq{ SELECT COUNT(1) FROM ( $sql ) AS t1 } ); - + $total_sth->execute( @{$p} ); ($count) = $total_sth->fetchrow_array(); $total_sth->finish; } - + if ( !$stillScanning ) { $cache->{$cacheKey} = $count; } @@ -5857,7 +5879,7 @@ sub videoTitlesQuery { if (main::VIDEO && main::MEDIASUPPORT) { my $loopname = 'videos_loop'; my $chunkCount = 0; - if ($valid) { + if ($valid) { # Limit the real query if ( $index =~ /^\d+$/ && $quantity =~ /^\d+$/ ) { $sql .= "LIMIT $index, $quantity "; @@ -5869,31 +5891,31 @@ sub videoTitlesQuery { if (main::VIDEO && main::MEDIASUPPORT) { my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p} ); - + # Bind selected columns in order my $i = 1; for my $col ( @cols ) { $sth->bind_col( $i++, \$c->{$col} ); } - + while ( $sth->fetch ) { if ( $sort ne 'new' ) { utf8::decode( $c->{'videos.titlesort'} ) if exists $c->{'videos.titlesort'}; } # "raw" result formatting (for CLI or JSON RPC) - $request->addResultLoop($loopname, $chunkCount, 'id', $c->{'videos.hash'}); + $request->addResultLoop($loopname, $chunkCount, 'id', $c->{'videos.hash'}); _videoData($request, $loopname, $chunkCount, $tags, $c); - + $chunkCount++; - + main::idleStreams() if !($chunkCount % 5); } } $request->addResult('count', $totalCount); - + $request->setStatusDone(); } } @@ -5926,9 +5948,9 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { $request->setStatusNotDispatchable(); return; } - + my $sqllog = main::DEBUGLOG && logger('database.sql'); - + # get our parameters my $index = $request->getParam('_index'); my $quantity = $request->getParam('_quantity'); @@ -5938,14 +5960,14 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { my $albums = $request->getParam('albums'); my $sort = $request->getParam('sort'); my $imageHash = $request->getParam('image_id'); - + #if ($sort && $request->paramNotOneOfIfDefined($sort, ['new'])) { # $request->setStatusBadParams(); # return; #} my $collate = Slim::Utils::OSDetect->getOS()->sqlHelperClass()->collate(); - + my $sql = 'SELECT %s FROM images '; my $c = { 'images.hash' => 1, 'images.titlesearch' => 1, 'images.titlesort' => 1 }; # columns my $w = []; # where @@ -5955,7 +5977,7 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { my $id_col = 'images.hash'; my $title_col= 'images.title'; my $limit; - + # Normalize and add any search parameters if ( defined $imageHash ) { push @{$w}, 'images.hash = ?'; @@ -5978,11 +6000,11 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { $order_by =~ s/;//g; # strip out any attempt at combining SQL statements } } - + if ( $timeline ) { $search ||= ''; my ($year, $month, $day) = split('-', $search); - + $tags = 't' if $timeline !~ /^(?:day|albums)$/; if ( $timeline eq 'years' ) { @@ -5990,7 +6012,7 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { $id_col = $order_by = $group_by = $title_col = 'year'; $c = { year => 1 }; } - + elsif ( $timeline eq 'months' && $year ) { $sql = sprintf $sql, "strftime('%m', date(original_time, 'unixepoch')) AS 'month'"; push @{$w}, "strftime('%Y', date(original_time, 'unixepoch')) == '$year'"; @@ -6016,18 +6038,18 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { $title_col = 'date'; $c = { date => 1, d => 1 }; } - + elsif ( $timeline eq 'day' && $year && $month && $day ) { push @{$w}, "date(original_time, 'unixepoch') == '$year-$month-$day'"; $timeline = ''; } } - + elsif ( $albums ) { if ( $search ) { $search = URI::Escape::uri_unescape($search); utf8::decode($search); - + $c->{'images.album'} = 1; push @{$w}, "images.album == ?"; push @{$p}, $search; @@ -6038,7 +6060,7 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { $tags = 't'; } } - + elsif ( $search && specified($search) ) { if ( $search =~ s/^sql=// ) { # Raw SQL search query @@ -6051,14 +6073,14 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { push @{$w}, '(' . join( ' OR ', map { 'images.titlesearch LIKE ?' } @{ $strings->[0] } ) . ')'; push @{$p}, @{ $strings->[0] }; } - else { + else { push @{$w}, 'images.titlesearch LIKE ?'; push @{$p}, @{$strings}; } } } } - + $tags =~ /t/ && do { $c->{$title_col} = 1 }; $tags =~ /o/ && do { $c->{'images.mime_type'} = 1 }; $tags =~ /f/ && do { $c->{'images.filesize'} = 1 }; @@ -6078,30 +6100,30 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { } $sql .= "GROUP BY $group_by " if $group_by; $sql .= "ORDER BY $order_by " if $order_by; - + # Add selected columns # Bug 15997, AS mapping needed for MySQL my @cols = keys %{$c}; $sql = sprintf $sql, join( ', ', map { $_ . " AS '" . $_ . "'" } @cols ) unless $timeline; - + my $stillScanning = Slim::Music::Import->stillScanning(); - + my $dbh = Slim::Schema->dbh; - + # Get count of all results, the count is cached until the next rescan done event - my $cacheKey = md5_hex($sql . join( '', @{$p} )); - + my $cacheKey = md5_hex($sql . join( '', @{$p} ) . (Slim::Utils::Text::ignoreCase($search, 1) || '')); + my $count = $cache->{$cacheKey}; if ( !$count ) { my $total_sth = $dbh->prepare_cached( qq{ SELECT COUNT(1) FROM ( $sql ) AS t1 } ); - + $total_sth->execute( @{$p} ); ($count) = $total_sth->fetchrow_array(); $total_sth->finish; } - + if ( !$stillScanning ) { $cache->{$cacheKey} = $count; } @@ -6120,7 +6142,7 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { my $loopname = 'images_loop'; my $chunkCount = 0; - if ($valid) { + if ($valid) { # Limit the real query if ( $index =~ /^\d+$/ && $quantity =~ /^\d+$/ ) { $sql .= "LIMIT $index, $quantity "; @@ -6132,36 +6154,36 @@ sub imageTitlesQuery { if (main::IMAGE && main::MEDIASUPPORT) { my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$p} ); - + # Bind selected columns in order my $i = 1; for my $col ( @cols ) { $sth->bind_col( $i++, \$c->{$col} ); } - + while ( $sth->fetch ) { utf8::decode( $c->{'images.title'} ) if exists $c->{'images.title'}; utf8::decode( $c->{'images.album'} ) if exists $c->{'images.album'}; - + if ( $sort ne 'new' ) { utf8::decode( $c->{'images.titlesort'} ) if exists $c->{'images.titlesort'}; } # "raw" result formatting (for CLI or JSON RPC) $request->addResultLoop($loopname, $chunkCount, 'id', $c->{$id_col}); - + $c->{title} = $c->{$title_col}; - + _imageData($request, $loopname, $chunkCount, $tags, $c); - + $chunkCount++; - + main::idleStreams() if !($chunkCount % 5); } } $request->addResult('count', $totalCount); - + $request->setStatusDone(); } } @@ -6181,7 +6203,7 @@ sub _imageData { if (main::IMAGE && main::MEDIASUPPORT) { $tags =~ /U/ && $request->addResultLoop($loopname, $chunkCount, 'updated_time', $c->{'images.updated_time'}); $tags =~ /l/ && $request->addResultLoop($loopname, $chunkCount, 'album', $c->{'images.album'}); $tags =~ /J/ && $request->addResultLoop($loopname, $chunkCount, 'hash', $c->{'images.hash'}); - + # browsing images by timeline Year -> Month -> Day $c->{year} && $request->addResultLoop($loopname, $chunkCount, 'year', $c->{'year'}); $c->{month} && $request->addResultLoop($loopname, $chunkCount, 'month', $c->{'month'}); diff --git a/Slim/Control/Request.pm b/Slim/Control/Request.pm index ee214a38302..4f41e446801 100644 --- a/Slim/Control/Request.pm +++ b/Slim/Control/Request.pm @@ -1,6 +1,6 @@ package Slim::Control::Request; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Control/Stdio.pm b/Slim/Control/Stdio.pm index a496839b217..9e08d6bff04 100644 --- a/Slim/Control/Stdio.pm +++ b/Slim/Control/Stdio.pm @@ -1,6 +1,6 @@ package Slim::Control::Stdio; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -60,7 +60,7 @@ sub processRequest { main::INFOLOG && $log->info("Got line: $firstline"); - my $message = executeCmd($firstline); + my $message = executeCmd($firstline) || ''; main::INFOLOG && $log->info("Response is: $message"); diff --git a/Slim/Control/XMLBrowser.pm b/Slim/Control/XMLBrowser.pm index f806080a728..4bdda67efb2 100644 --- a/Slim/Control/XMLBrowser.pm +++ b/Slim/Control/XMLBrowser.pm @@ -1,11 +1,9 @@ package Slim::Control::XMLBrowser; -# $Id: XMLBrowser.pm 23262 2008-09-23 19:21:03Z andy $ - -# Copyright 2005-2009 Logitech. +# Logitech Media Server Copyright 2005-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. =head1 NAME @@ -53,7 +51,7 @@ sub cliQuery { } $request->setStatusProcessing(); - + my $itemId = $request->getParam('item_id'); # get our parameters my $index = $request->getParam('_index'); my $quantity = $request->getParam('_quantity'); @@ -73,39 +71,39 @@ sub cliQuery { $quantity = 200; $request->addParam('_quantity', $quantity); } - + my $isPlayCommand = $request->isQuery([[$query], ['playlist']]); - + # Handle touch-to-play if ($request->getParam('touchToPlay') && !$request->getParam('xmlBrowseInterimCM') && (!$isPlayCommand || $request->getParam('_method') eq 'play')) { $isPlayCommand = 1; - + # A hack to handle clients that cannot map the 'go' action if (!$request->getParam('_method')) { $request->addParam('_method', 'play'); $request->addResult('goNow', 'nowPlaying'); } - + my $playalbum = undef; if ( $client ) { $playalbum = $prefs->client($client)->get('playtrackalbum'); } - + # if player pref for playtrack album is not set, get the old server pref. if ( !defined $playalbum ) { $playalbum = $prefs->get('playtrackalbum'); } - + if ($playalbum && !$request->getParam('touchToPlaySingle')) { $itemId =~ s/(.*)\.(\d+)/$1/; # strip off last node $request->addParam('playIndex', $2); # and save in playIndex $request->addParam('item_id', $itemId); } - + } - + my %args = ( 'request' => $request, 'client' => $client, @@ -117,14 +115,14 @@ sub cliQuery { # If the feed is already XML data (e.g., local music CMs, favorites menus), send it to handleFeed if ( ref $feed eq 'HASH' ) { - + main::DEBUGLOG && $log->debug("Feed is already XML data!"); - + $args{'url'} = $feed->{'url'}; _cliQuery_done( $feed, \%args ); return; } - + # Some plugins may give us a callback we should use to get OPML data # instead of fetching it ourselves. if ( ref $feed eq 'CODE' ) { @@ -147,12 +145,12 @@ sub cliQuery { _cliQuery_done( $opml, \%args ); }; - + my %args = (params => $request->getParamsCopy(), isControl => 1); # If we are getting an intermediate level, then we just need the one item # If we are getting the last level then we need all items if we are doing playall of some kind - + my $levels = 0; my $nextIndex; if ( defined $itemId && length($itemId) ) { @@ -163,14 +161,14 @@ sub cliQuery { $levels = scalar @index; ($nextIndex) = $index[0] =~ /^(\d+)/; } - + if (defined $index && $quantity && !$levels && !$isPlayCommand) { if (defined $request->getParam('feedMode')) { $args{'index'} = $index; $args{'quantity'} = $quantity; } else { # hack to allow for some CM entries - my $j = 10; + my $j = 10; $j = $index if ($j > $index); $args{'index'} = $index - $j; $args{'quantity'} = $quantity + $j; @@ -179,63 +177,63 @@ sub cliQuery { $args{'index'} = $nextIndex; $args{'quantity'} = 1; } - + if ($request->getParam('menu')) { if (my $sort = $prefs->get('jivealbumsort')) { $args{'orderBy'} = 'sort:' . $sort; } } - - + + if ( main::DEBUGLOG && $log->is_debug ) { my $cbname = Slim::Utils::PerlRunTime::realNameForCodeRef( $feed ); $log->debug( "Fetching OPML from coderef $cbname" ); } $feed->( $client, $callback, \%args); - + return; } - + if ( $feed =~ /{QUERY}/ ) { # Support top-level search my $query = $request->getParam('search'); - + if ( !$query ) { ($query) = $itemId =~ m/^_([^.]+)/; } - + $feed =~ s/{QUERY}/$query/g; $args{'url'} = $feed; } - + # Lookup this browse session in cache if user is browsing below top-level # This avoids repated lookups to drill down the menu if ( $itemId && $itemId =~ /^([a-f0-9]{8})/ ) { my $sid = $1; - + # Do not use cache if this is a search query if ( $request->getParam('search') ) { # Generate a new sid my $newsid = Slim::Utils::Misc::createUUID(); - + $itemId =~ s/^$sid/$newsid/; $request->addParam( item_id => "$itemId" ); # stringify for JSON } - + my $cache = Slim::Utils::Cache->new; if ( my $cached = $cache->get("xmlbrowser_$sid") ) { main::DEBUGLOG && $log->is_debug && $log->debug( "Using cached session $sid" ); - + _cliQuery_done( $cached, \%args ); return; } } main::DEBUGLOG && $log->debug("Asynchronously fetching feed $feed - will be back!"); - + Slim::Formats::XML->getFeedAsync( \&_cliQuery_done, \&_cliQuery_error, @@ -283,7 +281,7 @@ sub _cliQuery_done { # my $forceTitle = $params->{'forceTitle'}; my $client = $request->client(); my $window; - + main::INFOLOG && $log->info("_cliQuery_done(): ", $request->getRequestString()); my $cache = Slim::Utils::Cache->new; @@ -291,7 +289,7 @@ sub _cliQuery_done { my $isItemQuery = my $isPlaylistCmd = 0; my $xmlBrowseInterimCM = $request->getParam('xmlBrowseInterimCM'); my $xmlbrowserPlayControl = $request->getParam('xmlbrowserPlayControl'); - + if ($request->isQuery([[$query], ['playlist']])) { $isPlaylistCmd = 1; } @@ -312,11 +310,11 @@ sub _cliQuery_done { my $menu = $request->getParam('menu'); my $url = $request->getParam('url'); my $trackId = $request->getParam('track_id'); - + # menu/jive mgmt my $menuMode = defined $menu; my $feedMode = defined $request->getParam('feedMode'); - + my $playalbum = undef; if ( $client ) { $playalbum = $prefs->client($client)->get('playtrackalbum'); @@ -325,18 +323,18 @@ sub _cliQuery_done { if ( !defined $playalbum ) { $playalbum = $prefs->get('playtrackalbum'); } - + # Session ID for this browse session my $sid; - + # select the proper list of items my @index = (); if ( defined $item_id && length($item_id) ) { main::DEBUGLOG && $log->is_debug && $log->debug("item_id: $item_id"); - + @index = split /\./, $item_id; - + if ( length( $index[0] ) >= 8 && $index[0] =~ /^[a-f0-9]{8}/ ) { # Session ID is first element in index $sid = shift @index; @@ -344,23 +342,23 @@ sub _cliQuery_done { } else { my $refs = scalar grep { ref $_->{url} } @{ $feed->{items} }; - + # Don't cache if list has coderefs if ( !$refs ) { $sid = Slim::Utils::Misc::createUUID(); } } - + my $subFeed = $feed; $subFeed->{'offset'} ||= 0; - + my @crumbIndex = $sid ? ( $sid ) : (); - + # Add top-level search to index if ( defined $search && !scalar @index ) { @crumbIndex = ( ($sid || '') . '_' . uri_escape_utf8( $search, "^A-Za-z0-9" ) ); } - + if ( $sid ) { # Cache the feed structure for this session @@ -373,35 +371,35 @@ sub _cliQuery_done { $log->debug("Session not cached: $@"); } } - + if ( my $levels = scalar @index ) { # descend to the selected item my $depth = 0; - + for my $i ( @index ) { main::DEBUGLOG && $log->debug("Considering item $i"); $depth++; - + my ($in) = $i =~ /^(\d+)/; $subFeed = $subFeed->{'items'}->[$in - $subFeed->{'offset'}]; $subFeed->{'offset'} ||= 0; - + push @crumbIndex, $i; - + $search = $subFeed->{'searchParam'} if (defined $subFeed->{'searchParam'}); - + # Add search query to crumb list if ( $subFeed->{type} && $subFeed->{type} eq 'search' && defined $search ) { # Escape periods in the search string $crumbIndex[-1] .= '_' . uri_escape_utf8( $search, "^A-Za-z0-9" ); } - + # Change URL if there is a play attribute and it's the last item - if ( + if ( $subFeed->{play} - && $depth == $levels + && $depth == $levels && $isPlaylistCmd ) { $subFeed->{url} = $subFeed->{play}; @@ -409,7 +407,7 @@ sub _cliQuery_done { } # Change URL if there is a playlist attribute and it's the last item - if ( + if ( $subFeed->{playlist} && $depth == $levels && $isPlaylistCmd @@ -417,7 +415,7 @@ sub _cliQuery_done { $subFeed->{type} = 'playlist'; $subFeed->{url} = $subFeed->{playlist}; } - + # Bug 15343, if we are at the lowest menu level, and we have already # fetched and cached this menu level, check if we should always # re-fetch this menu. This is used to ensure things like the Pandora @@ -428,7 +426,7 @@ sub _cliQuery_done { main::DEBUGLOG && $log->is_debug && $log->debug(" Forcing refresh of menu"); delete $subFeed->{fetched}; } - + # If the feed is another URL, fetch it and insert it into the # current cached feed if ( (!$subFeed->{'type'} || ($subFeed->{'type'} ne 'audio')) && defined $subFeed->{'url'} && !$subFeed->{'fetched'} @@ -436,20 +434,20 @@ sub _cliQuery_done { # Only fetch playlist-with-parser types if playing - so favorites get the unsubstituted (long-lived) URL # # Unfortunately, we cannot do this because playtrackalbum & touchToPlay logic interfers by - # stripping the last component off the hierarchy. + # stripping the last component off the hierarchy. # && !($isItemQuery && $subFeed->{'type'} && $subFeed->{'type'} eq 'playlist' && $subFeed->{'parser'}) ) { - + if ( $i =~ /(?:\d+)?_(.+)/ ) { $search = Slim::Utils::Unicode::utf8on(uri_unescape($1)); } - + # Rewrite the URL if it was a search request if ( $subFeed->{type} && $subFeed->{type} eq 'search' && defined $search ) { my $encoded = URI::Escape::uri_escape_utf8($search); $subFeed->{url} =~ s/{QUERY}/$encoded/g; } - + # Setup passthrough args my $args = { 'item' => $subFeed, @@ -465,9 +463,9 @@ sub _cliQuery_done { 'expires' => $params->{'expires'}, 'timeout' => $params->{'timeout'}, }; - + if ( ref $subFeed->{url} eq 'CODE' ) { - + # Some plugins may give us a callback we should use to get OPML data # instead of fetching it ourselves. my $callback = sub { @@ -485,27 +483,27 @@ sub _cliQuery_done { items => (ref $data ne 'ARRAY' ? [$data] : $data), }; } - + _cliQuerySubFeed_done( $opml, $args ); }; - + my $pt = $subFeed->{passthrough} || []; my %args = (params => $feed->{'query'}, isControl => 1); - + if (defined $search && $subFeed->{type} && ($subFeed->{type} eq 'search' || defined $subFeed->{'searchParam'})) { $args{'search'} = $search; } - + # If we are getting an intermediate level, then we just need the one item # If we are getting the last level then we need all items if we are doing playall of some kind - + if (defined $index && $quantity && $depth == $levels && !$isPlaylistCmd) { if ($feedMode) { $args{'index'} = $index; $args{'quantity'} = $quantity; } else { # hack to allow for some CM entries - my $j = 10; + my $j = 10; $j = $index if ($j > $index); $args{'index'} = $index - $j; $args{'quantity'} = $quantity + $j; @@ -514,7 +512,7 @@ sub _cliQuery_done { $args{'index'} = $index[$depth]; $args{'quantity'} = 1; } - + if ( main::DEBUGLOG && $log->is_debug ) { my $cbname = Slim::Utils::PerlRunTime::realNameForCodeRef( $subFeed->{url} ); $log->debug( "Fetching OPML from coderef $cbname" ); @@ -522,10 +520,10 @@ sub _cliQuery_done { $subFeed->{url}->( $client, $callback, \%args, @{$pt}); } - + # No need to check for a cached version of this subfeed URL as getFeedAsync() will do that - else { + else { main::DEBUGLOG && $log->debug("Asynchronously fetching subfeed " . $subFeed->{url} . " - will be back!"); Slim::Formats::XML->getFeedAsync( @@ -534,31 +532,31 @@ sub _cliQuery_done { $args, ); } - + return; } # If the feed is an audio feed, Podcast enclosure or information item, display the info - # This is a leaf item, so show as much info as we have and go packing after that. + # This is a leaf item, so show as much info as we have and go packing after that. if ( $isItemQuery && ( - ($subFeed->{'type'} && $subFeed->{'type'} eq 'audio') || + ($subFeed->{'type'} && $subFeed->{'type'} eq 'audio') || $subFeed->{'enclosure'} || - # Bug 17385 - rss feeds include description at non leaf levels + # Bug 17385 - rss feeds include description at non leaf levels ($subFeed->{'description'} && $subFeed->{'type'} ne 'rss') ) ) { - + if ($feedMode) { $request->setRawResults($feed); $request->setStatusDone(); return; } - + main::DEBUGLOG && $log->debug("Adding results for audio or enclosure subfeed"); my ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), 1); - + my $cnt = 0; if ($menuMode) { @@ -570,13 +568,13 @@ sub _cliQuery_done { } } } # $menuMode - + else { $request->addResult('count', 1); } - + if ($valid) { - + my $loopname = $menuMode ? 'item_loop' : 'loop_loop'; $request->addResult('offset', $start) if $menuMode; @@ -587,33 +585,33 @@ sub _cliQuery_done { $hash{'id'} = "$item_id"; # stringify for JSON $hash{'name'} = $subFeed->{'name'} if defined $subFeed->{'name'}; $hash{'title'} = $subFeed->{'title'} if defined $subFeed->{'title'}; - - $hash{'isaudio'} = defined(hasAudio($subFeed)) + 0; - + + $hash{'isaudio'} = defined(hasAudio($subFeed)) + 0; + foreach my $data (keys %{$subFeed}) { - + if (ref($subFeed->{$data}) eq 'ARRAY') { # if (scalar @{$subFeed->{$data}}) { # $hash{'hasitems'} = scalar @{$subFeed->{$data}}; # } } - + elsif ($data =~ /enclosure/i && defined $subFeed->{$data}) { - + foreach my $enclosuredata (keys %{$subFeed->{$data}}) { if ($subFeed->{$data}->{$enclosuredata}) { $hash{$data . '_' . $enclosuredata} = $subFeed->{$data}->{$enclosuredata}; } } } - + elsif ($subFeed->{$data} && $data !~ /^(name|title|parser|fetched)$/) { $hash{$data} = $subFeed->{$data}; } } - + if ($menuMode) { - + foreach my $att (@mapAttributes[1..$#mapAttributes]) { my $key = $hash{$att->{'key'}}; next unless (defined $key && ($att->{'condition'} ? $att->{'condition'}->($key) : $key)); @@ -624,10 +622,10 @@ sub _cliQuery_done { $request->addResultLoop($loopname, $cnt, 'action', 'none'); $cnt++; } - + $request->addResult('count', $cnt); } # $menuMode - + else { $request->setResultLoopHash($loopname, $cnt, \%hash); } @@ -637,20 +635,20 @@ sub _cliQuery_done { } # $isItemQuery && (audio || enclosure || description) } } - + if ($feedMode) { $request->setRawResults($feed); $request->setStatusDone(); return; } - + if ($isPlaylistCmd) { # get our parameters my $method = $request->getParam('_method'); - + my $playIndex = $request->getParam('playIndex'); - + # playIndex will only be defined if we modified the item-Id earlier, for touchToPlay if ($request->getParam('touchToPlay') && defined($playIndex)) { if ($method =~ /^(add|play)$/ && $subFeed->{'items'}->[$playIndex]->{playall}) { @@ -668,24 +666,24 @@ sub _cliQuery_done { if ((defined $subFeed->{'url'} && $subFeed->{'type'} eq 'audio' || defined $subFeed->{'enclosure'}) && (defined $subFeed->{'name'} || defined $subFeed->{'title'}) && ($method !~ /all/)) { - + my $title = $subFeed->{'name'} || $subFeed->{'title'}; my $url = $subFeed->{'url'}; - + # Podcast enclosures if ( my $enc = $subFeed->{'enclosure'} ) { $url = $enc->{'url'}; } - + # Items with a 'play' attribute will use this for playback if ( my $play = $subFeed->{'play'} ) { $url = $play; } - + if ( $url ) { main::INFOLOG && $log->info("$method $url"); - + # Set metadata about this URL Slim::Music::Info::setRemoteMetadata( $url, { title => $title, @@ -694,20 +692,20 @@ sub _cliQuery_done { bitrate => $subFeed->{'bitrate'}, cover => $subFeed->{'cover'} || $subFeed->{'image'} || $subFeed->{'icon'} || $request->getParam('icon'), } ); - + $client->execute([ 'playlist', $method, $url ]); } else { main::INFOLOG && $log->info("No valid URL found for: ", $title); } } - + # play all streams of an item (or one stream if pref is unset) else { my @urls; for my $item ( @{ $subFeed->{'items'} } ) { my $url; - + if ( $item->{'type'} eq 'audio' && $item->{'url'} ) { $url = $item->{'url'}; } @@ -717,7 +715,7 @@ sub _cliQuery_done { elsif ( $item->{'play'} ) { $url = $item->{'play'}; } - + # Don't add non-audio items # In touch-to-play, only add items with the playall attribute if (!$url || defined($playIndex) && !$item->{'playall'}) { @@ -733,12 +731,12 @@ sub _cliQuery_done { bitrate => $item->{'bitrate'}, cover => $subFeed->{'cover'} || $subFeed->{'image'} || $subFeed->{'icon'} || $request->getParam('icon'), } ); - + main::idleStreams(); - + push @urls, $url; } - + if ( @urls ) { my $cmd; @@ -751,11 +749,11 @@ sub _cliQuery_done { $cmd = 'inserttracks'; $playIndex = undef; } - + if ( main::INFOLOG && $log->is_info ) { $log->info(sprintf("Playing/adding all items:\n%s", join("\n", @urls))); } - + $client->execute([ 'playlist', $cmd, 'listref', \@urls, undef, $playIndex ]); # if we're adding or inserting, show a showBriefly @@ -768,7 +766,7 @@ sub _cliQuery_done { else { main::INFOLOG && $log->info("No valid URL found for: ", ($subFeed->{'name'} || $subFeed->{'title'})); } - + } } else { @@ -780,13 +778,13 @@ sub _cliQuery_done { elsif ($isItemQuery) { main::INFOLOG && $log->info("Get items."); - + my $items = $subFeed->{'items'}; my $count = $subFeed->{'total'};; $count ||= defined $items ? scalar @$items : 0; - + # now build the result - + my $hasImage = 0; my $windowStyle; my $presetFavSet = 0; @@ -794,7 +792,7 @@ sub _cliQuery_done { my $allTouchToPlay = 1; my $defeatDestructiveTouchToPlay = _defeatDestructiveTouchToPlay($request, $client); my %actionParamsNeeded; - + if ($menuMode && defined $xmlbrowserPlayControl) { $totalCount = 0; @@ -803,14 +801,18 @@ sub _cliQuery_done { $log->error("Requested item index $xmlbrowserPlayControl out of range: ", $subFeed->{'offset'}, '..', $subFeed->{'offset'} + $count -1); } else { + # this certainly does look wrong, but I've seen cases where this menu was misbehavioung if there was a code reference in one item - mh + unshift @crumbIndex, 'ffffffff' if $crumbIndex[0] !~ /^[a-f0-9]{8}/i; + my $item = $items->[$i]; - for my $eachmenu (@{ + for my $eachmenu (@{ _playlistControlContextMenu({ request => $request, query => $query, item => $item, subFeed => $subFeed, noFavorites => 1, + item_id => scalar @crumbIndex ? join('.', @crumbIndex) : undef, subItemId => $xmlbrowserPlayControl, playalbum => 1, # Allways add play-all item }) @@ -819,19 +821,19 @@ sub _cliQuery_done { $request->setResultLoopHash('item_loop', $totalCount, $eachmenu); $totalCount++; } - + } $request->addResult('offset', 0); } - + elsif ($menuMode || $count || $xmlBrowseInterimCM) { - + # Bug 7024, display an "Empty" item instead of returning an empty list if ( $menuMode && !$count && !$xmlBrowseInterimCM) { $items = [ { type => 'text', name => $request->string('EMPTY') } ]; $totalCount = $count = 1; } - + my $loopname = $menuMode ? 'item_loop' : 'loop_loop'; my $cnt = 0; @@ -843,7 +845,7 @@ sub _cliQuery_done { if ($xmlBrowseInterimCM && !$subFeed->{'menuComplete'}) { for my $eachmenu (@{ _playlistControlContextMenu({ request => $request, query => $query, item => $subFeed }) }) { $totalCount = _fixCount(1, \$index, \$quantity, $totalCount); - + # Only add them the first time if ($firstChunk) { $request->setResultLoopHash('item_loop', $cnt, $eachmenu); @@ -851,15 +853,15 @@ sub _cliQuery_done { } } } - + } my ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), $count); - + if ($valid) { - + my $feedActions = $subFeed->{'actions'}; - + # Title is preferred here as it will contain the real title from the subfeed, # whereas name is the title of the menu item that led to this submenu and may # not always match @@ -869,29 +871,29 @@ sub _cliQuery_done { } $request->addResult( 'title', $title ); } - + # decide what is the next step down # we go to xxx items from xx items :) my $base; my $params = {}; - + if ($menuMode) { if (!$feedActions->{'allAvailableActionsDefined'}) { # build the default base element $params = { 'menu' => $query, }; - + if ( $url ) { $params->{'url'} = $url; } - + if ($feed->{'query'}) { $params = {%$params, %{$feed->{'query'}}}; } elsif ( $trackId ) { $params->{'track_id'} = $trackId; } - + $base = { 'actions' => { 'go' => { @@ -928,12 +930,12 @@ sub _cliQuery_done { }, }; } - + if (my $feedActions = $subFeed->{'actions'}) { my $n = 0; - + my $baseAction; - + if ($baseAction = _makeAction($feedActions, 'info', \%actionParamsNeeded, 1, 1)) { $base->{'actions'}->{'more'} = $baseAction; $n++; } @@ -953,12 +955,12 @@ sub _cliQuery_done { if ($baseAction = _makeAction($feedActions, 'insert', \%actionParamsNeeded, 1)) { $base->{'actions'}->{'add-hold'} = $baseAction; $n++; } - + if ($n >= 5) { $feedActions->{'allAvailableActionsDefined'} = 1; } } - + $base->{'actions'}->{'playControl'} = { player => 0, window => {isContextMenu => 1}, @@ -966,7 +968,7 @@ sub _cliQuery_done { itemsParams => 'playControlParams', params => $request->getParamsCopy(), }; - + $request->addResult('base', $base); } @@ -991,63 +993,64 @@ sub _cliQuery_done { my $itemIndex = $start - 1; my $format = $prefs->get('titleFormat')->[ $prefs->get('titleFormatWeb') ]; - + $start -= $subFeed->{'offset'}; $end -= $subFeed->{'offset'}; main::DEBUGLOG && $log->is_debug && $log->debug("Getting slice $start..$end: $totalCount; offset=", $subFeed->{'offset'}, ' quantity=', scalar @$items); - + my $search = $subFeed->{type} && $subFeed->{type} eq 'search'; - + my $baseId = scalar @crumbIndex ? join('.', @crumbIndex, '') : ''; for my $item ( @$items[$start..$end] ) { $itemIndex++; - + if ($item->{ignore}) { # Skip this item $totalCount--; next; } - + my $id = $baseId . $itemIndex; - - my $name; - if ($name = $item->{name}) { + + my $name = $item->{name}; + if (defined $name && $name ne '') { if (defined $item->{'label'}) { $name = $request->string($item->{'label'}) . $request->string('COLON') . ' ' . $name; } elsif (!$search && ($item->{'hasMetadata'} || '') eq 'track') { $name = Slim::Music::TitleFormatter::infoFormat(undef, $format, 'TITLE', $item) || $name; } } - + my $isPlayable = ( - $item->{play} - || $item->{playlist} + $item->{play} + || $item->{playlist} || ($item->{type} && ($item->{type} eq 'audio' || $item->{type} eq 'playlist')) ); - + # keep track of station icons - if ( - $isPlayable - && $item->{url} =~ /^http/ - && $item->{url} !~ m|\.com/api/\w+/v1/opml| - && (my $cover = ($item->{image} || $item->{cover})) + if ( + $isPlayable + && $item->{url} && !ref $item->{url} + && $item->{url} =~ /^http/ + && $item->{url} !~ m|\.com/api/\w+/v1/opml| + && (my $cover = ($item->{image} || $item->{cover})) && !Slim::Utils::Cache->new->get("remote_image_" . $item->{url}) ) { $cache->set("remote_image_" . $item->{url}, $cover, 86400); } - + if ($menuMode) { my %hash; - + $hash{'type'} = $item->{'type'} if defined $item->{'type'}; # search|text|textarea|audio|playlist|link|opml|replace|redirect|radio - # radio is a radio-button selection item, not an internet-radio station - my $nameOrTitle = $name || $item->{title} || ''; + # radio is a radio-button selection item, not an internet-radio station + my $nameOrTitle = getTitle($name, $item); my $touchToPlay = defined(touchToPlay($item)) + 0; - + # if showBriefly is 1, send the name as a showBriefly if ($item->{showBriefly} and ( $nameOrTitle ) ) { - $client->showBriefly({ + $client->showBriefly({ 'jive' => { 'type' => 'popupplay', 'text' => [ $nameOrTitle ], @@ -1064,20 +1067,20 @@ sub _cliQuery_done { if ($item->{nowPlaying}) { $request->addResult('goNow', 'nowPlaying'); } - + # wrap = 1 and type = textarea render in the single textarea area above items if ( $item->{name} && $item->{wrap} || $item->{type} && $item->{type} eq 'textarea' ) { $window->{textarea} = $item->{name}; # Skip this item $totalCount--; - + # In case this is the only item, add an empty item list $request->setResultLoopHash($loopname, 0, {}); - + next; } - + # Avoid including album tracks and the like in context-menus if ( $xmlBrowseInterimCM && ($item->{play} || ($item->{type} && ($item->{type} eq 'audio'))) @@ -1088,23 +1091,23 @@ sub _cliQuery_done { $totalCount--; next; } - + # Bug 13175, support custom windowStyle - this is really naff if ( $item->{style} ) { $windowStyle = $item->{style}; } - + # Bug 7077, if the item will autoplay, it has an 'autoplays=1' attribute if ( $item->{autoplays} ) { $hash{'style'} = 'itemplay'; } - + elsif (my $playcontrol = $item->{'playcontrol'}) { if ($playcontrol eq 'play') {$hash{'style'} = 'item_play';} elsif ($playcontrol eq 'add') {$hash{'style'} = 'item_add';} elsif ($playcontrol eq 'insert' && $client->revision !~ /^7\.[0-7]/) {$hash{'style'} = 'item_insert';} } - + my $itemText = $nameOrTitle; if ($item->{'name2'}) { $itemText .= "\n" . $item->{'name2'}; @@ -1115,7 +1118,7 @@ sub _cliQuery_done { $itemText = ( $item->{line1} || $nameOrTitle ) . "\n" . $line2; } $hash{'text'} = $itemText; - + if ($isPlayable) { my $presetParams = _favoritesParams($item); if ($presetParams && !$xmlBrowseInterimCM) { @@ -1126,14 +1129,14 @@ sub _cliQuery_done { my $itemParams = {}; - if ( !$item->{type} || $item->{type} ne 'text' ) { + if ( !$item->{type} || $item->{type} ne 'text' ) { $itemParams->{'item_id'} = "$id", #stringify, make sure it's a string } if ( $isPlayable || $item->{isContextMenu} ) { $itemParams->{'isContextMenu'} = 1; } - + if ($item->{type} && $item->{type} eq 'slideshow') { $itemParams->{slideshow} = 1; } @@ -1156,7 +1159,7 @@ sub _cliQuery_done { $hash{'style'} ||= 'itemNoAction'; $hash{'action'} = 'none'; } - + if ( $item->{type} && $item->{type} eq 'localservice' ) { $hash{'actions'} = { go => { @@ -1167,7 +1170,7 @@ sub _cliQuery_done { elsif ( $item->{type} && $item->{type} eq 'search' ) { #$itemParams->{search} = '__INPUT__'; - + # XXX: bug in Jive, this should really be handled by the base go action my $actions = { go => { @@ -1180,12 +1183,12 @@ sub _cliQuery_done { }, }, }; - + # Allow search results to become a slideshow if ( defined $item->{slideshow} ) { $actions->{go}->{params}->{slideshow} = $item->{slideshow}; } - + my $input = { len => 1, processingPopup => { @@ -1198,7 +1201,7 @@ sub _cliQuery_done { softbutton2 => $request->string('DELETE'), title => $item->{title} || $item->{name}, }; - + $hash{'actions'} = $actions; $hash{'input'} = $input; if ($item->{nextWindow}) { @@ -1206,13 +1209,13 @@ sub _cliQuery_done { } $allTouchToPlay = 0; } - + elsif ( !$isPlayable && !$touchToPlay && ($hash{'style'} || '') ne 'itemNoAction') { - + # I think that doing it this way means that, because $itemParams does not get # added as 'params' if !$isPlayable, therefore all the other default actions will # bump because SlimBrowser needs 'params' as specified in the base actions. - + my $actions = { 'go' => { 'cmd' => [ $query, 'items' ], @@ -1231,41 +1234,41 @@ sub _cliQuery_done { $hash{'addAction'} = 'go'; $allTouchToPlay = 0; } - + elsif ( $touchToPlay ) { if (!$defeatDestructiveTouchToPlay) { $itemParams->{'touchToPlay'} = "$id"; # stringify, make sure it's a string $itemParams->{'touchToPlaySingle'} = 1 if !$item->{'playall'}; - + # not currently supported by 7.5 client - $hash{'goAction'} = 'play'; - + $hash{'goAction'} = 'play'; + $hash{'style'} = 'itemplay'; } else { # not currently supported by 7.5 client - $hash{'goAction'} = 'playControl'; + $hash{'goAction'} = 'playControl'; $hash{'playControlParams'} = {xmlbrowserPlayControl=>"$itemIndex"}; } } else { $allTouchToPlay = 0; } - + my $itemActions = $item->{'itemActions'}; if ($itemActions) { - + my $actions; if (!$itemActions->{'allAvailableActionsDefined'}) { $actions = $hash{'actions'}; } $actions ||= {}; - + my $n = 0; - + if (my $action = _makeAction($itemActions, 'info', undef, 1, 1)) { $actions->{'more'} = $action; $n++; } - + # Need to be careful not to undo (effectively) a 'go' action mapping # (could also consider other mappings but do not curretly) my $goAction = $hash{'goAction'}; @@ -1279,7 +1282,7 @@ sub _cliQuery_done { } if (my $action = _makeAction($itemActions, 'play', undef, 1, 0, 'nowPlaying')) { $actions->{'play'} = $action; $n++; - + # This test should really be repeated for all the other actions, # in case 'go' is mapped to one of them, but that does not actually # happen (would have to be somewhere in this module) @@ -1295,7 +1298,7 @@ sub _cliQuery_done { $actions->{'add-hold'} = $action; $n++; } $hash{'actions'} = $actions; - + if ($n >= 5) { $itemActions->{'allAvailableActionsDefined'} = 1; } @@ -1311,73 +1314,74 @@ sub _cliQuery_done { $hash{$key} = \%params; } } - + if ( !$itemActions->{'allAvailableActionsDefined'} && !$feedActions->{'allAvailableActionsDefined'} && scalar keys %{$itemParams} && ($isPlayable || $touchToPlay) ) { $hash{'params'} = $itemParams; } - + if ( $item->{jive} ) { my $actions = $hash{'actions'} || {}; while (my($name, $action) = each(%{$item->{jive}->{actions} || {}})) { $actions->{$name} = $action; } $hash{'actions'} = $actions; - + for my $key ('window', 'showBigArtwork', 'style', 'nextWindow') { if ( $item->{jive}->{$key} ) { $hash{$key} = $item->{jive}->{$key}; } } - + $hash{'icon-id'} = proxiedImage($item->{jive}->{'icon-id'}) if $item->{jive}->{'icon-id'}; } - + if (exists $hash{'actions'} && scalar keys %{$hash{'actions'}}) { delete $hash{'action'}; delete $hash{'style'} if $hash{'style'} && $hash{'style'} eq 'itemNoAction'; } - + $hash{'textkey'} = $item->{textkey} if defined $item->{textkey}; - + $request->setResultLoopHash($loopname, $cnt, \%hash); } else { # create an ordered hash to store this stuff... tie my %hash, "Tie::IxHash"; - + $hash{id} = $id; $hash{name} = $name if defined $name; $hash{type} = $item->{type} if defined $item->{type}; $hash{title} = $item->{title} if defined $item->{title}; $hash{image} = proxiedImage($item->{image}) if defined $item->{image}; + $hash{image} ||= proxiedImage($item->{icon}) if defined $item->{icon}; # add url entries if requested unless they are coderefs as this breaks serialisation if ($want_url && defined $item->{url} && (!ref $item->{url} || ref $item->{url} ne 'CODE')) { $hash{url} = $item->{url}; - } + } $hash{isaudio} = defined(hasAudio($item)) + 0; - + # Bug 7684, set hasitems to 1 if any of the following are true: # type is not text or audio # items array contains items { my $hasItems = 0; - + if ( !defined $item->{type} || $item->{type} !~ /^(?:text|audio)$/i ) { $hasItems = 1; } elsif ( ref $item->{items} eq 'ARRAY' ) { $hasItems = scalar @{ $item->{items} }; } - + $hash{hasitems} = $hasItems; } - + $request->setResultLoopHash($loopname, $cnt, \%hash); } $cnt++; @@ -1387,29 +1391,29 @@ sub _cliQuery_done { } $request->addResult('count', $totalCount); - + if ($menuMode) { - + if ($request->getResult('base')) { my $baseActions = $request->getResult('base')->{'actions'}; - + _jivePresetBase($baseActions) if $presetFavSet; - + if ($allTouchToPlay) { $baseActions->{'go'} = $defeatDestructiveTouchToPlay ? $baseActions->{'playControl'} : $baseActions->{'play'}; } } - + if ( $windowStyle ) { $window->{'windowStyle'} = $windowStyle; - } + } elsif ( $hasImage ) { $window->{'windowStyle'} = 'home_menu'; - } + } else { $window->{'windowStyle'} = 'text_list'; } - + # Bug 13247, support windowId param if ( $subFeed->{windowId} ) { $window->{windowId} = $subFeed->{windowId}; @@ -1422,7 +1426,7 @@ sub _cliQuery_done { # cache SBC queries for "Recent Search" menu if ($search && ($request->getParam('cachesearch') || $subFeed->{'cachesearch'})) { # Bug 13044, allow some searches to not be cached - + # XXX this is probably obsolete because of move to myapps # make a best effort to make a labeled title for the search my $queryTypes = { @@ -1430,12 +1434,11 @@ sub _cliQuery_done { mp3tunes => 'PLUGIN_MP3TUNES_MODULE_NAME', radiotime => 'PLUGIN_RADIOTIME_MODULE_NAME', slacker => 'PLUGIN_SLACKER_MODULE_NAME', - live365 => 'PLUGIN_LIVE365_MODULE_NAME', lma => 'PLUGIN_LMA_MODULE_NAME', }; - + my $title = $search; - + if ($queryTypes->{$query}) { $title = $request->string($queryTypes->{$query}) . ": " . $title; } elsif (my $key = $subFeed->{'cachesearch'}) { @@ -1444,7 +1447,7 @@ sub _cliQuery_done { $title = $key . ': ' . $title; } } - + my $queryParams = $feed->{'query'} || {}; my $jiveSearchCache = { text => $title, @@ -1461,14 +1464,14 @@ sub _cliQuery_done { }, }, }; - + Slim::Control::Jive::cacheSearch($request, $jiveSearchCache); } - + } } # ENDIF $isItemQuery - + $request->setStatusDone(); } @@ -1477,15 +1480,15 @@ sub _cliQuery_done { # After fetching, insert the contents into the original feed sub _cliQuerySubFeed_done { my ( $feed, $params ) = @_; - + # If there's a command we need to run, run it. This is used in various # places to trigger actions from an OPML result, such as to start playing # a new Pandora radio station if ( $feed->{command} ) { - + my @p = map { uri_unescape($_) } split / /, $feed->{command}; my $client = $params->{request}->client(); - + if ($client) { main::DEBUGLOG && $log->is_debug && $log->debug( "Executing command: " . Data::Dump::dump(\@p) ); $client->execute( \@p ); @@ -1493,22 +1496,22 @@ sub _cliQuerySubFeed_done { $log->error('No client to execute command for.'); } } - + # insert the sub-feed data into the original feed my $parent = $params->{'parent'}; my $subFeed = $parent; - + for my $i ( @{ $params->{'currentIndex'} } ) { # Skip sid and sid + top-level search query next if length($i) >= 8 && $i =~ /^[a-f0-9]{8}/; - + # If an index contains a search query, strip it out $i =~ s/_.+$//g; - + $subFeed = $subFeed->{'items'}->[$i - ($subFeed->{'offset'} || 0)]; } - if (($subFeed->{'type'} && $subFeed->{'type'} eq 'replace' || $feed->{'replaceparent'}) && + if (($subFeed->{'type'} && $subFeed->{'type'} eq 'replace' || $feed->{'replaceparent'}) && $feed->{'items'} && scalar @{$feed->{'items'}} == 1) { # if child has 1 item and requests, update previous entry to avoid new menu level delete $subFeed->{'url'}; @@ -1516,19 +1519,19 @@ sub _cliQuerySubFeed_done { for my $key (keys %$item) { $subFeed->{ $key } = $item->{ $key }; } - } + } else { # otherwise insert items as subfeed $subFeed->{'items'} = $feed->{'items'}; } $subFeed->{'fetched'} = 1; - + # Pass-through forceRefresh flag if ( $feed->{forceRefresh} ) { $subFeed->{forceRefresh} = 1; } - + # Support alternate title if it's different from this menu in the parent if ( $feed->{title} && $subFeed->{name} ne $feed->{title} ) { main::DEBUGLOG && $log->is_debug && $log->debug("menu title was '" . $subFeed->{name} . "', changing to '" . $feed->{title} . "'"); @@ -1540,7 +1543,7 @@ sub _cliQuerySubFeed_done { $subFeed->{'total'} = $feed->{'total'}; $subFeed->{'offset'} = $feed->{'offset'}; $subFeed->{'menuComplete'} = $feed->{'menuComplete'}; - + # Mark this as coming from subFeed, so that we know to ignore forceRefresh $params->{fromSubFeed} = 1; @@ -1548,7 +1551,7 @@ sub _cliQuerySubFeed_done { if (defined $feed->{'cachetime'}) { $parent->{'cachetime'} = min( $parent->{'cachetime'} || CACHE_TIME, $feed->{'cachetime'} ); } - + _cliQuery_done( $parent, $params ); } @@ -1562,11 +1565,11 @@ sub _addingToPlaylist { ? $client->string('ADDING_TO_PLAYLIST') : $client->string('INSERT_TO_PLAYLIST'); - my $jivestring = $action eq 'add' + my $jivestring = $action eq 'add' ? $client->string('JIVE_POPUP_ADDING') : $client->string('JIVE_POPUP_TO_PLAY_NEXT'); - $client->showBriefly( { + $client->showBriefly( { line => [ $string ], jive => { type => 'mixed', @@ -1579,7 +1582,7 @@ sub _addingToPlaylist { sub findAction { my ($feed, $item, $actionName) = @_; - + if ($item && $item->{'itemActions'} && $item->{'itemActions'}->{$actionName}) { return wantarray ? ($item->{'itemActions'}->{$actionName}, {}) : $item->{'itemActions'}->{$actionName}; } @@ -1594,10 +1597,10 @@ sub findAction { sub _makePlayAction { my ($subFeed, $item, $name, $nextWindow, $query, $mode, $item_id, $playIndex) = @_; - + my %params; my $cmd; - + if (my ($feedAction, $feedActions) = findAction($subFeed, $item, $name)) { %params = %{$feedAction->{'fixedParams'}} if $feedAction->{'fixedParams'}; my @vars = exists $feedAction->{'variables'} ? @{$feedAction->{'variables'}} : @{$feedActions->{'commonVariables'} || []}; @@ -1612,10 +1615,10 @@ sub _makePlayAction { ); $params{'playIndex'} = $playIndex if defined $playIndex; $params{'mode'} = $mode if defined $mode; - + $cmd = [ $query, 'playlist', $name ], } - + if ($cmd) { $params{'menu'} = 1; @@ -1625,29 +1628,29 @@ sub _makePlayAction { params => \%params, ); $action{'nextWindow'} = $nextWindow if $nextWindow; - + return \%action; } - + return undef; } sub _makeAction { my ($actions, $actionName, $actionParamsNeeded, $player, $contextMenu, $nextWindow) = @_; - + if (my $action = $actions->{$actionName}) { if ( !($action->{'command'} && scalar @{$action->{'command'}}) ) { return 'none'; } - + my $params = $action->{'fixedParams'} || {}; $params->{'menu'} ||= 1; - + my %action = ( cmd => $action->{'command'}, params => $params, ); - + $action{'player'} ||= 0 if $player; $action{'window'} = {isContextMenu => 1} if $contextMenu; $action{'nextWindow'} = $nextWindow if $nextWindow; @@ -1668,16 +1671,16 @@ sub _makeAction { sub _cliQuery_error { my ( $err, $params ) = @_; - + my $request = $params->{'request'}; my $url = $params->{'url'}; - + logError("While retrieving [$url]: [$err]"); - + $request->addResult("networkerror", $err); $request->addResult('count', 0); - $request->setStatusDone(); + $request->setStatusDone(); } # fix the count in case we're adding additional items @@ -1710,14 +1713,14 @@ sub _fixCount { sub hasAudio { my $item = shift; - + if ( $item->{'play'} ) { return $item->{'play'}; } elsif ( $item->{'type'} && $item->{'type'} =~ /^(?:audio|playlist)$/ ) { return $item->{'playlist'} || $item->{'url'} || scalar @{ $item->{outline} || [] }; } - elsif ( $item->{'enclosure'} && ( $item->{'enclosure'}->{'type'} =~ /audio/ ) ) { + elsif ( $item->{'enclosure'} && $item->{'enclosure'}->{'type'} && ( $item->{'enclosure'}->{'type'} =~ /audio/ ) ) { return $item->{'enclosure'}->{'url'}; } else { @@ -1727,7 +1730,7 @@ sub hasAudio { sub touchToPlay { my $item = shift; - + if ( $item->{'type'} && $item->{'type'} =~ /^(?:audio)$/ ) { return 1; } @@ -1786,33 +1789,33 @@ sub _playlistControlContextMenu { my @contextMenu; my $canIcons = $request && $request->client && ($request->client->revision !~ /^7\.[0-7]/); - + # We only add playlist-control items for an item which is playable if (hasAudio($item)) { - my $item_id = $request->getParam('item_id') || ''; + my $item_id = $args->{item_id} || $request->getParam('item_id') || ''; my $mode = $request->getParam('mode'); my $sub_id = $args->{'subItemId'}; my $subFeed = $args->{'subFeed'}; - + if (defined $sub_id) { $item_id .= '.' if length($item_id); $item_id .= $sub_id; } - + my $itemParams = { menu => $request->getParam('menu'), item_id => $item_id, }; - + my $addPlayAll = ( $args->{'playalbum'} && defined $sub_id && $subFeed && scalar @{$subFeed->{'items'} || []} > 1 && ($subFeed->{'playall'} || $item->{'playall'}) ); - + my $action; - + if ($action = _makePlayAction($subFeed, $item, 'add', 'parentNoRefresh', $query, $mode, $item_id)) { push @contextMenu, { text => $request->string('ADD_TO_END'), @@ -1820,7 +1823,7 @@ sub _playlistControlContextMenu { actions => {go => $action}, }, } - + if ($action = _makePlayAction($subFeed, $item, 'insert', 'parentNoRefresh', $query, $mode, $item_id)) { push @contextMenu, { text => $request->string('PLAY_NEXT'), @@ -1828,7 +1831,7 @@ sub _playlistControlContextMenu { actions => {go => $action}, }, } - + if ($action = _makePlayAction($subFeed, $item, 'play', 'nowPlaying', $query, $mode, $item_id)) { push @contextMenu, { text => $request->string($addPlayAll ? 'PLAY_THIS_SONG' : 'PLAY'), @@ -1836,8 +1839,8 @@ sub _playlistControlContextMenu { actions => {go => $action}, }, } - - if ($addPlayAll && ($action = _makePlayAction($subFeed, $item, 'playall', 'nowPlaying', $query, $mode, $request->getParam('item_id'), $sub_id))) { + + if ($addPlayAll && ($action = _makePlayAction($subFeed, $item, 'playall', 'nowPlaying', $query, $mode, $args->{item_id} || $request->getParam('item_id'), $sub_id))) { push @contextMenu, { text => $request->string('JIVE_PLAY_ALL_SONGS'), style => $canIcons ? 'item_playall' : 'itemNoAction', @@ -1849,7 +1852,7 @@ sub _playlistControlContextMenu { # Favorites handling my $favParams; if (($favParams = _favoritesParams($item)) && !$args->{'noFavorites'}) { - + my $action = 'add'; my $favIndex = undef; my $token = 'JIVE_SAVE_TO_FAVORITES'; @@ -1861,7 +1864,7 @@ sub _playlistControlContextMenu { $token = 'JIVE_DELETE_FROM_FAVORITES'; } } - + my $favoriteActions = { 'go' => { player => 0, @@ -1879,7 +1882,7 @@ sub _playlistControlContextMenu { if (my $icon = $favParams->{'icon'} || $request->getParam('icon')) { $favoriteActions->{'go'}{'params'}{'icon'} = $icon; } - + push @contextMenu, { text => $request->string($token), style => $canIcons ? 'item_fav' : 'itemNoAction', @@ -1892,26 +1895,26 @@ sub _playlistControlContextMenu { sub _favoritesParams { my $item = shift; - + my $favorites_url = $item->{favorites_url} || $item->{play} || $item->{url}; my $favorites_title = $item->{title} || $item->{name}; - + if ( $favorites_url && !ref $favorites_url && $favorites_title ) { if ( !$item->{favorites_url} && $item->{type} && $item->{type} eq 'playlist' && $item->{playlist} && !ref $item->{playlist}) { $favorites_url = $item->{playlist}; } - + my %presetParams = ( favorites_url => $favorites_url, favorites_title => $favorites_title, favorites_type => $item->{favorites_type} || ($item->{play} ? 'audio' : ($item->{type} || 'audio')), ); $presetParams{'parser'} = $item->{'parser'} if $item->{'parser'}; - + if (my $icon = $item->{'image'} || $item->{'icon'} || $item->{'cover'}) { $presetParams{'icon'} = proxiedImage($icon); } - + return \%presetParams; } } @@ -1919,36 +1922,48 @@ sub _favoritesParams { sub _defeatDestructiveTouchToPlay { my ($request, $client) = @_; my $pref; - + if ($client && (my $agent = $client->controllerUA)) { - if ($agent =~ /squeezeplay/i) { + if ($agent =~ /squeezeplay/i && $agent !~ /jivelite/i) { my ($version, $revision) = ($agent =~ m%/(\d+(?:\.\d+)?)[.\d]*-r(\d+)%); - + return 0 if $version < 7.6; return 0 if $version eq '7.6' && $revision < 9337; } } - + $pref = $request->getParam('defeatDestructiveTouchToPlay'); $pref = $prefs->client($client)->get('defeatDestructiveTouchToPlay') if $client && !defined $pref; $pref = $prefs->get('defeatDestructiveTouchToPlay') if !defined $pref; - + # Values: # 0 => no defeat # 1 => always defeat # 2 => defeat if playlist length > 1 # 3 => defeat only if playing and current-playlist-length > 1 # 4 => defeat only if playing and current item not a radio stream - + return 0 if !$pref; return 1 if $pref == 1 || !$client; return ($client->isPlaying() && $client->playingSong()->duration() && !$client->playingSong()->isPlaylist()) if $pref == 4; my $l = Slim::Player::Playlist::count($client); return 0 if $l < 2; return 0 if $pref == 3 && (!$client->isPlaying() || $l < 2); - + return 1; } +# a name can be '0' (zero) - don't blank it +sub getTitle { + my ($name, $item) = @_; + + my $nameOrTitle = $name; + $nameOrTitle = $item->{title} if !defined $nameOrTitle || $nameOrTitle eq ''; + $nameOrTitle = '' if !defined $nameOrTitle; + + return $nameOrTitle; +} + + 1; diff --git a/Slim/Display/Boom.pm b/Slim/Display/Boom.pm index 7722e883fb1..4b07abfbba9 100644 --- a/Slim/Display/Boom.pm +++ b/Slim/Display/Boom.pm @@ -1,11 +1,10 @@ package Slim::Display::Boom; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME diff --git a/Slim/Display/Display.pm b/Slim/Display/Display.pm index 685210644e5..6be3f1990c3 100644 --- a/Slim/Display/Display.pm +++ b/Slim/Display/Display.pm @@ -1,8 +1,7 @@ package Slim::Display::Display; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Display/EmulatedSqueezebox2.pm b/Slim/Display/EmulatedSqueezebox2.pm index e1e95f149cb..b222026d391 100644 --- a/Slim/Display/EmulatedSqueezebox2.pm +++ b/Slim/Display/EmulatedSqueezebox2.pm @@ -1,12 +1,10 @@ package Slim::Display::EmulatedSqueezebox2; -# Logitech Media Server Copyright (c) 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id: EmulatedSqueezebox2.pm 13042 2007-09-17 22:44:40Z adrian $ - =head1 NAME Slim::Display::EmulatedSqueezebox2 diff --git a/Slim/Display/Graphics.pm b/Slim/Display/Graphics.pm index db5be66937b..2c845cfb104 100644 --- a/Slim/Display/Graphics.pm +++ b/Slim/Display/Graphics.pm @@ -1,11 +1,10 @@ package Slim::Display::Graphics; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME diff --git a/Slim/Display/Lib/Fonts.pm b/Slim/Display/Lib/Fonts.pm index d2d61549e68..430212cb6ef 100644 --- a/Slim/Display/Lib/Fonts.pm +++ b/Slim/Display/Lib/Fonts.pm @@ -1,11 +1,10 @@ package Slim::Display::Lib::Fonts; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. # -# $Id$ # =head1 NAME diff --git a/Slim/Display/Lib/TextVFD.pm b/Slim/Display/Lib/TextVFD.pm index 9e0765beeff..5de3614d828 100644 --- a/Slim/Display/Lib/TextVFD.pm +++ b/Slim/Display/Lib/TextVFD.pm @@ -1,11 +1,10 @@ package Slim::Display::Lib::TextVFD; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. # -# $Id$ # =head1 NAME diff --git a/Slim/Display/NoDisplay.pm b/Slim/Display/NoDisplay.pm index 1922fbbc8a3..aea29c93f84 100644 --- a/Slim/Display/NoDisplay.pm +++ b/Slim/Display/NoDisplay.pm @@ -1,11 +1,10 @@ package Slim::Display::NoDisplay; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME diff --git a/Slim/Display/Squeezebox2.pm b/Slim/Display/Squeezebox2.pm index e03e623bcfa..d2366d3ab29 100644 --- a/Slim/Display/Squeezebox2.pm +++ b/Slim/Display/Squeezebox2.pm @@ -1,11 +1,10 @@ package Slim::Display::Squeezebox2; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME diff --git a/Slim/Display/SqueezeboxG.pm b/Slim/Display/SqueezeboxG.pm index 7a549c5d56e..0a9cf9c445d 100644 --- a/Slim/Display/SqueezeboxG.pm +++ b/Slim/Display/SqueezeboxG.pm @@ -1,11 +1,10 @@ package Slim::Display::SqueezeboxG; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME diff --git a/Slim/Display/Text.pm b/Slim/Display/Text.pm index 778a6a52c35..2da48550965 100644 --- a/Slim/Display/Text.pm +++ b/Slim/Display/Text.pm @@ -1,11 +1,10 @@ package Slim::Display::Text; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME diff --git a/Slim/Display/Transporter.pm b/Slim/Display/Transporter.pm index aede92fc010..7a38fc13d53 100644 --- a/Slim/Display/Transporter.pm +++ b/Slim/Display/Transporter.pm @@ -1,11 +1,10 @@ package Slim::Display::Transporter; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME diff --git a/Slim/Formats.pm b/Slim/Formats.pm index 50c6b6d55d6..4de53777147 100644 --- a/Slim/Formats.pm +++ b/Slim/Formats.pm @@ -1,8 +1,7 @@ package Slim::Formats; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -16,12 +15,13 @@ use Slim::Music::Info; use Slim::Utils::Log; use Slim::Utils::Misc; use Slim::Utils::Unicode; +use Slim::Utils::Versions; # Map our tag functions - so they can be dynamically loaded. our (%tagClasses, %loadedTagClasses); my $init = 0; -my $log = logger('formats'); +my $log = logger('formats.audio'); =head1 NAME @@ -58,8 +58,10 @@ sub init { 'wmap' => 'Slim::Formats::WMA', 'wmal' => 'Slim::Formats::WMA', 'alc' => 'Slim::Formats::Movie', + 'alcx' => 'Slim::Formats::Movie', 'aac' => 'Slim::Formats::Movie', 'mp4' => 'Slim::Formats::Movie', + 'mp4x' => 'Slim::Formats::Movie', 'sls' => 'Slim::Formats::Movie', 'shn' => 'Slim::Formats::Shorten', 'mpc' => 'Slim::Formats::Musepack', @@ -79,11 +81,15 @@ sub init { 'xpf' => 'Slim::Formats::Playlists::XSPF', ); - if ($Audio::Scan::VERSION =~ /^0\.9[45]$/) { + if (Slim::Utils::Versions->compareVersions($Audio::Scan::VERSION,'0.94') >= 0) { $tagClasses{'dff'} = 'Slim::Formats::DFF'; $tagClasses{'dsf'} = 'Slim::Formats::DSF'; } + if (Slim::Utils::Versions->compareVersions($Audio::Scan::VERSION, '1.02') >= 0) { + $tagClasses{'ops'} = 'Slim::Formats::OggOpus'; + } + $init = 1; return 1; @@ -250,6 +256,11 @@ sub readTags { ($tags->{'FILESIZE'}, $tags->{'TIMESTAMP'}) = (stat(_))[7,9]; } + if ($tags->{'LEADING_MDAT'}) { + $type = 'mp4x'; + $tags->{'CONTENT_TYPE'} = 'alcx' if ($tags->{'CONTENT_TYPE'} eq 'alc') + } + # Only set if we couldn't read it from the file. $tags->{'CONTENT_TYPE'} ||= $type; diff --git a/Slim/Formats/AIFF.pm b/Slim/Formats/AIFF.pm index 5adb247720a..c1a58517c20 100644 --- a/Slim/Formats/AIFF.pm +++ b/Slim/Formats/AIFF.pm @@ -1,8 +1,6 @@ package Slim::Formats::AIFF; -# $Id$ -# -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/APE.pm b/Slim/Formats/APE.pm index d01c68878d5..e897af5038c 100644 --- a/Slim/Formats/APE.pm +++ b/Slim/Formats/APE.pm @@ -1,8 +1,6 @@ package Slim::Formats::APE; -# $Id: APE.pm 5405 2005-12-14 22:02:37Z dean $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/FLAC.pm b/Slim/Formats/FLAC.pm index 12f5574f432..3e1d08cc55f 100644 --- a/Slim/Formats/FLAC.pm +++ b/Slim/Formats/FLAC.pm @@ -1,8 +1,6 @@ package Slim::Formats::FLAC; -# $tagsd: FLAC.pm,v 1.5 2003/12/15 17:57:50 daniel Exp $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/MP3.pm b/Slim/Formats/MP3.pm index 0cad8575a24..61b3d2053b0 100644 --- a/Slim/Formats/MP3.pm +++ b/Slim/Formats/MP3.pm @@ -1,8 +1,7 @@ package Slim::Formats::MP3; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/Movie.pm b/Slim/Formats/Movie.pm index 2816125b12d..89e87d2e6a7 100644 --- a/Slim/Formats/Movie.pm +++ b/Slim/Formats/Movie.pm @@ -1,8 +1,7 @@ package Slim::Formats::Movie; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -50,7 +49,7 @@ sub getTag { my $info = $s->{info}; my $tags = $s->{tags}; - + return unless $info->{song_length_ms}; # skip files with video tracks @@ -67,6 +66,7 @@ sub getTag { $tags->{SECS} = $info->{song_length_ms} / 1000; $tags->{BITRATE} = $info->{avg_bitrate}; $tags->{DLNA_PROFILE} = $info->{dlna_profile} || undef; + $tags->{LEADING_MDAT} = $info->{leading_mdat} || undef; if ( my $track = $info->{tracks}->[0] ) { # MP4 file diff --git a/Slim/Formats/Musepack.pm b/Slim/Formats/Musepack.pm index 5804a4d85da..7573575a3d1 100644 --- a/Slim/Formats/Musepack.pm +++ b/Slim/Formats/Musepack.pm @@ -1,8 +1,6 @@ package Slim::Formats::Musepack; -# $tagsd: Musepack.pm,v 1.0 2004/01/27 00:00:00 daniel Exp $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/Ogg.pm b/Slim/Formats/Ogg.pm index 2b616091e19..f1145642f74 100644 --- a/Slim/Formats/Ogg.pm +++ b/Slim/Formats/Ogg.pm @@ -1,8 +1,7 @@ package Slim::Formats::Ogg; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/OggOpus.pm b/Slim/Formats/OggOpus.pm new file mode 100644 index 00000000000..396d2252877 --- /dev/null +++ b/Slim/Formats/OggOpus.pm @@ -0,0 +1,150 @@ +package Slim::Formats::OggOpus; + +use strict; +use base qw(Slim::Formats::Ogg); + +use Fcntl qw(:seek); +use Slim::Utils::Log; +use Slim::Utils::Strings qw(string); + +use Audio::Scan; + +my $log = logger('scan.scanner'); +my $sourcelog = logger('player.source'); + +sub scanBitrate { + my $class = shift; + my $fh = shift; + my $url = shift; + + my $isDebug = $log->is_debug; + + local $ENV{AUDIO_SCAN_NO_ARTWORK} = 0; + + seek $fh, 0, 0; + + my $s = Audio::Scan->scan_fh( opus => $fh ); + + if ( !$s->{info}->{audio_offset} ) { + + logWarning('Unable to parse Opus stream'); + + return (-1, undef); + } + + my $info = $s->{info}; + my $tags = $s->{tags}; + + # Save tag data if available + if ( my $title = $tags->{TITLE} ) { + # XXX: Schema ignores ARTIST, ALBUM, YEAR, and GENRE for remote URLs + # so we have to format our title info manually. + my $track = Slim::Schema->updateOrCreate( { + url => $url, + attributes => { + TITLE => $title, + }, + } ); + + main::DEBUGLOG && $isDebug && $log->debug("Read Opus tags from stream: " . Data::Dump::dump($tags)); + + $title .= ' ' . string('BY') . ' ' . $tags->{ARTIST} if $tags->{ARTIST}; + $title .= ' ' . string('FROM') . ' ' . $tags->{ALBUM} if $tags->{ALBUM}; + + Slim::Music::Info::setCurrentTitle( $url, $title ); + + # Save artwork if found + # Read cover art if available + if ( $tags->{ALLPICTURES} ) { + my $coverart; + my $mime; + + my @allpics = sort { $a->{picture_type} <=> $b->{picture_type} } + @{ $tags->{ALLPICTURES} }; + + if ( my @frontcover = grep ( $_->{picture_type} == 3, @allpics ) ) { + # in case of many type 3 (front cover) just use the first one + $coverart = $frontcover[0]->{image_data}; + $mime = $frontcover[0]->{mime_type}; + } + else { + # fall back to use lowest type image found + $coverart = $allpics[0]->{image_data}; + $mime = $allpics[0]->{mime_type}; + } + + $track->cover( length($coverart) ); + $track->update; + + my $data = { + image => $coverart, + type => $tags->{COVERARTMIME} || $mime, + }; + + my $cache = Slim::Utils::Cache->new(); + $cache->set( "cover_$url", $data, 86400 * 7 ); + + main::DEBUGLOG && $isDebug && $log->debug( 'Found embedded cover art, saving for ' . $track->url ); + } + } + + my $vbr = 0; + + if ( defined $info->{bitrate_upper} && defined $info->{bitrate_lower} ) { + if ( $info->{bitrate_upper} != $info->{bitrate_lower} ) { + $vbr = 1; + } + } + + if ( my $bitrate = ( $info->{bitrate_average} || $info->{bitrate_nominal} ) ) { + + main::DEBUGLOG && $isDebug && $log->debug("Found bitrate header: $bitrate kbps " . ( $vbr ? 'VBR' : 'CBR' )); + + return ( $bitrate, $vbr ); + } + + logWarning("Unable to read bitrate from stream!"); + + return (-1, undef); +} + +sub getInitialAudioBlock { + my ($class, $fh) = @_; + + open my $localFh, '<&=', $fh; + + seek $localFh, 0, 0; + + my $s = Audio::Scan->scan_fh( opus => $localFh ); + + main::DEBUGLOG && $sourcelog->is_debug && $sourcelog->debug( 'Reading initial audio block: length ' . $s->{info}->{audio_offset} ); + + seek $localFh, 0, 0; + read $localFh, my $buffer, $s->{info}->{audio_offset}; + + close $localFh; + + return $buffer; +} + +=head2 findFrameBoundaries( $fh, $offset, $time ) + +Seeks to the Ogg block containing the sample at $time. + +The only caller is L at this time. + +=cut + +sub findFrameBoundaries { + my ( $class, $fh, $offset, $time ) = @_; + + if ( !defined $fh || !defined $time ) { + return 0; + } + + return Audio::Scan->find_frame_fh( opus => $fh, int($time * 1000) ); +} + +sub canSeek { 1 } + +1; diff --git a/Slim/Formats/Playlists.pm b/Slim/Formats/Playlists.pm index 7f0f4484fcb..934d18503a4 100644 --- a/Slim/Formats/Playlists.pm +++ b/Slim/Formats/Playlists.pm @@ -1,8 +1,7 @@ package Slim::Formats::Playlists; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License, version 2. diff --git a/Slim/Formats/Playlists/ASX.pm b/Slim/Formats/Playlists/ASX.pm index 77e8e3727f5..c009eb6a03b 100644 --- a/Slim/Formats/Playlists/ASX.pm +++ b/Slim/Formats/Playlists/ASX.pm @@ -1,8 +1,7 @@ package Slim::Formats::Playlists::ASX; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/Playlists/Base.pm b/Slim/Formats/Playlists/Base.pm index dd7c68e1fee..a2b4b1c06ae 100644 --- a/Slim/Formats/Playlists/Base.pm +++ b/Slim/Formats/Playlists/Base.pm @@ -1,8 +1,7 @@ package Slim::Formats::Playlists::Base; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License, version 2. @@ -55,6 +54,7 @@ sub _updateMetaData { 'url' => $entry, 'attributes' => $attributes, 'readTags' => 1, + 'playlist' => Slim::Music::Info::isPlaylist($entry), } ); } diff --git a/Slim/Formats/Playlists/CUE.pm b/Slim/Formats/Playlists/CUE.pm index 97880a174bf..9bfd068a2a1 100644 --- a/Slim/Formats/Playlists/CUE.pm +++ b/Slim/Formats/Playlists/CUE.pm @@ -1,8 +1,7 @@ package Slim::Formats::Playlists::CUE; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Formats/Playlists/M3U.pm b/Slim/Formats/Playlists/M3U.pm index 60d3b60de9e..79c455c150b 100644 --- a/Slim/Formats/Playlists/M3U.pm +++ b/Slim/Formats/Playlists/M3U.pm @@ -1,11 +1,10 @@ package Slim::Formats::Playlists::M3U; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -29,7 +28,7 @@ sub read { my @items = (); my ($secs, $artist, $album, $title, $trackurl); - my $foundBOM = 0; + my $checkedBOM = 0; my $fh; my $mediadirs; @@ -48,7 +47,7 @@ sub read { return @items; }; } - + main::INFOLOG && $log->info("Parsing M3U: $url"); while (my $entry = <$fh>) { @@ -56,12 +55,20 @@ sub read { chomp($entry); # strip carriage return from dos playlists - $entry =~ s/\cM//g; + $entry =~ s/\cM//g; # strip whitespace from beginning and end - $entry =~ s/^\s*//; - $entry =~ s/\s*$//; - + $entry =~ s/^\s*//; + $entry =~ s/\s*$//; + + # Only strip the BOM off of UTF-8 encoded bytes. Encode will + # handle UTF-16. Only check on first line. + if (!$checkedBOM && Slim::Utils::Unicode::encodingFromString($entry) eq 'utf8') { + $entry = Slim::Utils::Unicode::stripBOM($entry); + } + + $checkedBOM = 1; + # If the line is not a filename (starts with #), handle encoding # If it's a filename, accept it as raw bytes if ( $entry =~ /^#/ ) { @@ -72,14 +79,6 @@ sub read { # includes a playlist that has latin1 titles, and utf8 paths. my $enc = Slim::Utils::Unicode::encodingFromString($entry); - # Only strip the BOM off of UTF-8 encoded bytes. Encode will - # handle UTF-16 - if (!$foundBOM && $enc eq 'utf8') { - - $entry = Slim::Utils::Unicode::stripBOM($entry); - $foundBOM = 1; - } - $entry = Slim::Utils::Unicode::utf8decode_guess($entry, $enc); } @@ -98,26 +97,26 @@ sub read { elsif ($entry =~ /^#EXTINF:(.*?),(.*)$/) { $secs = $1; - $title = $2; + $title = $2; main::DEBUGLOG && $log->debug(" found secs: $secs, title: $title"); } elsif ( $entry =~ /^#EXTINF:(.*?)$/ ) { $title = $1; - + main::DEBUGLOG && $log->debug(" found title: $title"); } elsif ( $entry =~ /^#EXTURL:(.*?)$/ ) { $trackurl = $1; - + main::DEBUGLOG && $log->debug(" found trackurl: $trackurl"); } next if $entry =~ /^#/; next if $entry =~ /#CURTRACK/; next if $entry eq ""; - + # if an invalid playlist is downloaded as HTML, ignore it last if $entry =~ /^<(?:!DOCTYPE\s*)?html/; next if $entry =~ /^playlistEntryIsValid($trackurl, $url)) { push @items, $class->_item($trackurl, $artist, $album, $title, $secs, $url); @@ -148,14 +147,14 @@ sub read { else { # Check if the playlist entry is relative to audiodir $mediadirs ||= Slim::Utils::Misc::getAudioDirs(); - + foreach my $audiodir (@$mediadirs) { $trackurl = Slim::Utils::Misc::fixPath($entry, $audiodir); - + if ($class->playlistEntryIsValid($trackurl, $url)) { push @items, $class->_item($trackurl, $artist, $album, $title, $secs, $url); - + last; } } @@ -178,7 +177,7 @@ sub _item { my ($class, $trackurl, $artist, $album, $title, $secs, $playlistUrl) = @_; main::DEBUGLOG && $log->debug(" valid entry: $trackurl"); - + return $class->_updateMetaData( $trackurl, { 'TITLE' => $title, 'ALBUM' => $album, @@ -193,12 +192,12 @@ sub readCurTrackForM3U { # do nothing to the index if we can't open the list open(FH, $path) || return 0; - + # retrieve comment with track number in it my $line = ; close(FH); - + if ($line =~ /#CURTRACK (\d+)$/) { main::INFOLOG && $log->info("Found track: $1"); @@ -219,7 +218,7 @@ sub writeCurTrackForM3U { # do nothing to the index if we can't open the list open(IN, $path) || return 0; open(OUT, ">$path.tmp") || return 0; - + while (my $line = ) { if ($line =~ /#CURTRACK (\d+)$/) { @@ -261,24 +260,24 @@ sub write { for my $item (@{$listref}) { my $track = Slim::Schema->objectForUrl($item); - + if (!blessed($track) || !$track->can('title')) { - + if ( Slim::Music::Info::isURL($item) && $item !~ /^file:/ ) { print $output $item, "\n"; } else { logError("Couldn't retrieve objectForUrl: [$item] - skipping!"); } - + next; }; - + # Bug 16683: put the 'file:///' URL in an extra extension print $output "#EXTURL:", $track->url, "\n"; if ($addTitles) { - + my $title = $track->title; my $secs = int($track->secs || -1); @@ -286,10 +285,10 @@ sub write { print $output "#EXTINF:$secs,$title\n"; } } - + my $path = Slim::Utils::Unicode::utf8decode_locale( $class->_pathForItem($track->url) ); print $output $path, "\n"; - + main::idleStreams() if ! (++$i % 20); } diff --git a/Slim/Formats/Playlists/PLS.pm b/Slim/Formats/Playlists/PLS.pm index 7521ad9b4d7..0be4c00e460 100644 --- a/Slim/Formats/Playlists/PLS.pm +++ b/Slim/Formats/Playlists/PLS.pm @@ -1,8 +1,6 @@ package Slim::Formats::Playlists::PLS; -# $Id - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Formats/Playlists/WPL.pm b/Slim/Formats/Playlists/WPL.pm index 15f8a72da1b..bc6d0de294a 100644 --- a/Slim/Formats/Playlists/WPL.pm +++ b/Slim/Formats/Playlists/WPL.pm @@ -1,8 +1,7 @@ package Slim::Formats::Playlists::WPL; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Formats/Playlists/XML.pm b/Slim/Formats/Playlists/XML.pm index c6c838a4af9..687063d1279 100644 --- a/Slim/Formats/Playlists/XML.pm +++ b/Slim/Formats/Playlists/XML.pm @@ -1,8 +1,6 @@ package Slim::Formats::Playlists::XML; -# $Id - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Formats/Playlists/XSPF.pm b/Slim/Formats/Playlists/XSPF.pm index 61fcf5f0012..eb4452e1e06 100644 --- a/Slim/Formats/Playlists/XSPF.pm +++ b/Slim/Formats/Playlists/XSPF.pm @@ -1,8 +1,7 @@ package Slim::Formats::Playlists::XSPF; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Formats/RemoteMetadata.pm b/Slim/Formats/RemoteMetadata.pm index a17b2eb3cbf..9c49b1935c2 100644 --- a/Slim/Formats/RemoteMetadata.pm +++ b/Slim/Formats/RemoteMetadata.pm @@ -1,8 +1,7 @@ package Slim::Formats::RemoteMetadata; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -101,7 +100,7 @@ if you want the standard metadata functions to handle the data. Slim::Formats::RemoteMetadata->registerParser( match => qr/soma\.fm/, func => \&parser, - ) ); + ); sub parser { my ( $client, $url, $metadata ) = @_; diff --git a/Slim/Formats/RemoteStream.pm b/Slim/Formats/RemoteStream.pm index 39d632acebb..8bc54c0f18b 100644 --- a/Slim/Formats/RemoteStream.pm +++ b/Slim/Formats/RemoteStream.pm @@ -1,8 +1,7 @@ package Slim::Formats::RemoteStream; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License, version 2. @@ -67,7 +66,7 @@ sub open { main::INFOLOG && $log->info("Opening connection using proxy $proxy"); } - main::INFOLOG && $log->info("Opening connection to $url: [$server on port $port with path $path with timeout $timeout]"); + main::INFOLOG && $log->is_info && $log->info("Opening connection to $url: [$server on port $port with path $path with timeout $timeout]"); my $sock = $class->SUPER::new( LocalAddr => $main::localStreamAddr, diff --git a/Slim/Formats/WMA.pm b/Slim/Formats/WMA.pm index b976c197b7f..5839ca1c765 100644 --- a/Slim/Formats/WMA.pm +++ b/Slim/Formats/WMA.pm @@ -1,8 +1,7 @@ package Slim::Formats::WMA; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/Wav.pm b/Slim/Formats/Wav.pm index f3c5bf3b4f1..24271e8ac23 100644 --- a/Slim/Formats/Wav.pm +++ b/Slim/Formats/Wav.pm @@ -1,8 +1,7 @@ package Slim::Formats::Wav; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Formats/XML.pm b/Slim/Formats/XML.pm index 2a54f585584..989bbbdb7a5 100644 --- a/Slim/Formats/XML.pm +++ b/Slim/Formats/XML.pm @@ -1,11 +1,9 @@ package Slim::Formats::XML; -# $Id$ - -# Copyright 2006-2009 Logitech +# Logitech Media Server Copyright 2006-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. # This class handles retrieval and parsing of remote XML feeds (OPML and RSS) @@ -15,7 +13,7 @@ use File::Slurp; use HTML::Entities; use JSON::XS::VersionOneAndTwo; use Scalar::Util qw(weaken); -use URI::Escape qw(uri_escape uri_escape_utf8); +use URI::Escape qw(uri_escape_utf8); use XML::Simple; use Slim::Music::Info; @@ -27,6 +25,8 @@ use Slim::Utils::Log; use Slim::Utils::Prefs; use Slim::Utils::Strings qw(string); +use constant IS_TUNEIN_RE => qr/(?:radiotime|tunein)\.com/i; + # How long to cache parsed XML data our $XML_CACHE_TIME = 300; @@ -35,19 +35,19 @@ my $prefs = preferences('server'); sub _cacheKey { my ( $url, $client ) = @_; - + my $cachekey = $url; - + if ($client) { - $cachekey .= '-' . $client->languageOverride; + $cachekey .= '-' . ($client->languageOverride || ''); } - + return $cachekey . '_parsedXML'; } sub getCachedFeed { my ( $class, $url, $client ) = @_; - + my $cache = Slim::Utils::Cache->new(); return $cache->get( _cacheKey($url, $client) ); } @@ -55,9 +55,9 @@ sub getCachedFeed { sub getFeedAsync { my $class = shift; my ( $cb, $ecb, $params ) = @_; - + my $url = $params->{'url'}; - + # Try to load a cached copy of the parsed XML my $cache = Slim::Utils::Cache->new(); my $feed = $cache->get( _cacheKey($url, $params->{client}) ); @@ -68,7 +68,7 @@ sub getFeedAsync { return $cb->( $feed, $params ); } - + if (Slim::Music::Info::isFileURL($url)) { my $path = Slim::Utils::Misc::pathFromFileURL($url); @@ -86,10 +86,38 @@ sub getFeedAsync { } } + # if we have a single item, we might need to expand it to some list (eg. Spotify Album -> track list) + my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url) unless $feed; + + if ( $handler && $handler->can('explodePlaylist') ) { + $handler->explodePlaylist($params->{client}, $url, sub { + my ($tracks) = @_; + + return $cb->({ + 'type' => 'opml', + 'title' => '', + 'items' => [ + map { + { + # compatable with INPUT.Choice, which expects 'name' and 'value' + 'name' => $_, + 'value' => $_, + 'url' => $_, + 'type' => 'audio', + 'items' => [], + } + } @{$tracks || []} + ], + }, $params); + }); + + return; + } + if ($feed) { return $cb->( $feed, $params ); } - + my $http = Slim::Networking::SimpleAsyncHTTP->new( \&gotViaHTTP, \&gotErrorViaHTTP, { @@ -102,7 +130,7 @@ sub getFeedAsync { }); main::INFOLOG && $log->is_info && $log->info("Async request: $url"); - + # Bug 3165 # Override user-agent and Icy-Metadata headers so we appear to be a web browser my $ua = Slim::Utils::Misc::userAgentString(); @@ -113,26 +141,27 @@ sub getFeedAsync { 'Icy-Metadata' => '', ); - if ( $url =~ /(?:radiotime|tunein\.com)/ ) { + if ( $url =~ IS_TUNEIN_RE ) { # Add the TuneIn username if ( $url !~ /username/ && $url =~ /(?:presets|title)/ + && Slim::Utils::PluginManager->isEnabled('Slim::Plugin::InternetRadio::Plugin') && ( my $username = Slim::Plugin::InternetRadio::TuneIn->getUsername($params->{client}) ) ) { $url .= '&username=' . uri_escape_utf8($username); } } - + # If the URL is on SqueezeNetwork, add session headers or login first if ( !main::NOMYSB && Slim::Networking::SqueezeNetwork->isSNURL($url) && !$params->{no_sn} ) { - + # Sometimes from the web we won't have a client, so pick a random one $params->{client} ||= Slim::Player::Client::clientRandom(); - + my %snHeaders = Slim::Networking::SqueezeNetwork->getHeaders( $params->{client} ); while ( my ($k, $v) = each %snHeaders ) { $headers{$k} = $v; } - + # Don't require SN session for public URLs if ( $url !~ m|/public/| ) { main::INFOLOG && $log->is_info && $log->info("URL requires SqueezeNetwork session"); @@ -142,13 +171,13 @@ sub getFeedAsync { $ecb->( string('SQUEEZENETWORK_NO_PLAYER_CONNECTED'), $params ); return; } - + if ( my $snCookie = Slim::Networking::SqueezeNetwork->getCookie( $params->{client} ) ) { $headers{Cookie} = $snCookie; } else { main::INFOLOG && $log->is_info && $log->info("Logging in to SqueezeNetwork to obtain session ID"); - + # Login and get a session ID Slim::Networking::SqueezeNetwork->login( client => $params->{client}, @@ -158,7 +187,7 @@ sub getFeedAsync { main::INFOLOG && $log->is_info && $log->info('Got SqueezeNetwork session ID'); } - + $http->get( $url, %headers ); }, ecb => sub { @@ -166,7 +195,7 @@ sub getFeedAsync { $ecb->( $error, $params ); }, ); - + return; } } @@ -179,7 +208,7 @@ sub gotViaHTTP { my $http = shift; my $params = $http->params(); my $feed; - + my $ct = $http->headers()->content_type; if ( main::DEBUGLOG && $log->is_debug ) { @@ -246,7 +275,7 @@ sub gotViaHTTP { $ecb->( '{PARSE_ERROR}', $params->{'params'} ); return; } - + # Cache the parsed XML or raw response if ( Slim::Utils::Misc::shouldCacheURL( $http->url ) ) { @@ -319,14 +348,14 @@ sub parseXMLIntoFeed { my $type = shift || 'text/xml'; my $xml; - + if ( $type =~ /json/ ) { $xml = from_json($$content); } else { $xml = xmlToHash($content); } - + # convert XML into data structure if ($xml && $xml->{'body'}) { @@ -334,9 +363,11 @@ sub parseXMLIntoFeed { # its OPML outline return parseOPML($xml); - + } elsif ($xml && $xml->{'entry'}) { - + + main::DEBUGLOG && $log->is_debug && $log->debug("Parsing body as Atom"); + # It's Atom return parseAtom($xml); @@ -365,22 +396,43 @@ sub parseRSS { 'managingEditor' => unescapeAndTrim($xml->{'channel'}->{'managingEditor'}), 'xmlns:slim' => unescapeAndTrim($xml->{'xmlsns:slim'}), ); - - # look for an image + + # Look for an image + + # Note: we take special care to ensure that "$feed{'image'}" is only ever + # populated with a *scalar* value. Anything else will break Jive browsing + # (SlimBrowserApplet) when it attempts to fetch artwork. + # E.g. If a broken podcast provides an empty 'url' tag, 'XMLin' would interpret it + # as an empty hash ref. So we explicitly guard against such occurrences. + if ( ref $xml->{'channel'}->{'image'} ) { - + my $image = $xml->{'channel'}->{'image'}; - my $url = $image->{'url'}; - + my $url = ""; + if (ref $image eq 'HASH') { + # conventional RSS feeds simply provide one image element + $url = $image->{'url'}; + } + elsif (ref $image eq 'ARRAY') { + # some RSS feeds provide several variant image elements + # we pick the first one + $url = $image->[0]->{'url'}; + } + # some Podcasts have the image URL in the link tag if ( !$url && $image->{'link'} && $image->{'link'} =~ /(jpg|gif|png)$/i ) { $url = $image->{'link'}; } - - $feed{'image'} = $url; + + $feed{'image'} = $url unless ref $url; # scalar value only ! } - elsif ( $xml->{'itunes:image'} ) { - $feed{'image'} = $xml->{'itunes:image'}->{'href'}; + elsif ( ref $xml->{'itunes:image'} eq 'HASH' ) { + my $href = $xml->{'itunes:image'}->{'href'}; + $feed{'image'} = $href unless ref $href; + } + elsif ( ref $xml->{'channel'}->{'itunes:image'} eq 'HASH' ) { + my $href = $xml->{'channel'}->{'itunes:image'}->{'href'}; + $feed{'image'} = $href unless ref $href; } # some feeds (slashdot) have items at same level as channel @@ -413,12 +465,19 @@ sub parseRSS { $item{'duration'} = unescapeAndTrim($itemXML->{'itunes:duration'}); $item{'explicit'} = unescapeAndTrim($itemXML->{'itunes:explicit'}); + # Use episode specific image if there is one. + if (ref $itemXML->{'itunes:image'} eq 'HASH') { + my $href = $itemXML->{'itunes:image'}->{'href'}; + # We only want a non null scalar + $item{'image'} = $href if $href && ! ref $href; + } + # don't duplicate data - if ( $itemXML->{'itunes:subtitle'} && $itemXML->{'title'} && + if ( $itemXML->{'itunes:subtitle'} && $itemXML->{'title'} && $itemXML->{'itunes:subtitle'} ne $itemXML->{'title'} ) { $item{'subtitle'} = unescapeAndTrim($itemXML->{'itunes:subtitle'}); } - + if ( $itemXML->{'itunes:summary'} && $itemXML->{'description'} && $itemXML->{'itunes:summary'} ne $itemXML->{'description'} ) { $item{'summary'} = unescapeAndTrim($itemXML->{'itunes:summary'}); @@ -443,21 +502,21 @@ sub parseRSS { push @{$feed{'items'}}, \%item; } - + return \%feed; } # Parse Atom feeds into the same format as RSS sub parseAtom { my $xml = shift; - + # Handle text constructs for my $field ( qw(title subtitle tagline) ) { if ( ref $xml->{$field} eq 'HASH' ) { $xml->{$field} = $xml->{$field}->{content}; } } - + # Support Person construct if ( ref $xml->{author} eq 'HASH' ) { my $name = $xml->{author}->{name}; @@ -466,7 +525,7 @@ sub parseAtom { } $xml->{author} = $name; } - + my %feed = ( 'type' => 'rss', 'items' => [], @@ -476,25 +535,25 @@ sub parseAtom { 'managingEditor' => unescapeAndTrim($xml->{'author'}), 'xmlns:slim' => unescapeAndTrim($xml->{'xmlsns:slim'}), ); - + # look for an image if ( $xml->{'logo'} ) { $feed{'image'} = $xml->{'logo'}; } - + my $count = 1; - + my $items = $xml->{'entry'} || []; for my $itemXML ( @{$items} ) { - + # Handle text constructs for my $field ( qw(summary title) ) { if ( ref $itemXML->{$field} eq 'HASH' ) { $itemXML->{$field} = $itemXML->{$field}->{content}; } } - + my %item = ( 'description' => unescapeAndTrim($itemXML->{'summary'}), 'title' => unescapeAndTrim($itemXML->{'title'}), @@ -505,6 +564,22 @@ sub parseAtom { 'image' => $feed{'image'}, ); + # some Atom streams come with multiple link items, one of them pointing to the stream (enclosure) + # create a valid enclosure element our XMLBrowser implementations understand + if ( !$item{link} && $itemXML->{link} && ref $itemXML->{link} && ref $itemXML->{link} eq 'ARRAY' ) { + my @links = grep { + $_->{rel} && lc($_->{rel}) eq 'enclosure' + } @{$itemXML->{link}}; + + if (scalar @links) { + $item{enclosure} = { + url => $links[0]->{href}, + type => $links[0]->{type}, + duration => $itemXML->{'itunes:duration'} + }; + } + } + # this is a convencience for using INPUT.Choice later. # it expects each item in it list to have some 'value' $item{'value'} = $count++; @@ -518,7 +593,7 @@ sub parseAtom { # represent OPML in a simple data structure compatable with INPUT.Choice mode. sub parseOPML { my $xml = shift; - + my $head = $xml->{head}; my $opml = { @@ -526,36 +601,36 @@ sub parseOPML { 'title' => unescapeAndTrim($head->{'title'}), 'items' => _parseOPMLOutline($xml->{'body'}->{'outline'}), }; - + # Optional command to run (used by Pandora) if ( $xml->{'command'} ) { $opml->{'command'} = $xml->{'command'}; - + # Optional flag to abort OPML processing after command is run $opml->{abort} = $xml->{abort} if $xml->{abort}; } - + # Optional item to indicate if the list is sorted if ( $xml->{sorted} ) { $opml->{sorted} = $xml->{sorted}; } - + # respect cache time as returned by the data source if ( defined $head->{cachetime} ) { $opml->{cachetime} = $head->{cachetime} + 0; } - + # Optional windowId to support nextWindow if ( $head->{windowId} ) { $opml->{windowId} = $head->{windowId}; } - + # Bug 15343, a menu may define forceRefresh in the head to always # be refreshed when accessing this menu item if ( $head->{forceRefresh} ) { $opml->{forceRefresh} = 1; } - + $xml = undef; # Don't leak @@ -574,11 +649,13 @@ sub _parseOPMLOutline { my $url = $itemXML->{'url'} || $itemXML->{'URL'} || $itemXML->{'xmlUrl'}; + next if $url && $url =~ IS_TUNEIN_RE && $itemXML && ref $itemXML && $itemXML->{key} && $itemXML->{key} eq 'unavailable'; + # Some programs, such as OmniOutliner put garbage in the URL. if ($url) { $url =~ s/^.*?<(\w+:\/\/.+?)>.*$/$1/; } - + # Pull in all attributes we find my %attrs; for my $attr ( keys %{$itemXML} ) { @@ -617,12 +694,12 @@ sub xmlToHash { $@ = "Invalid XML feed\n"; } else { - + # make 2 passes at parsing: # 1. Parse content as-is # 2. Try decoding invalid characters for my $pass ( 1..2 ) { - + if ( $pass == 2 ) { # Some feeds have invalid (usually Windows encoding) in a UTF-8 XML file. my @lines = (); @@ -647,7 +724,7 @@ sub xmlToHash { # keyattr => [] prevents id attrs from overriding $xml = XMLin( ref $content ? $content : \$content, 'forcearray' => [qw(item outline entry)], 'keyattr' => []); }; - + if ($@) { $log->warn("Pass $pass failed to parse: $@"); } @@ -689,7 +766,7 @@ sub unescape { # Decode all entities in-place decode_entities($data); - + # Unescape URI (some Odeo OPML needs this) $data =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; diff --git a/Slim/GUI/ControlPanel.pm b/Slim/GUI/ControlPanel.pm index 5feb1fdff5d..fd068c9baa6 100644 --- a/Slim/GUI/ControlPanel.pm +++ b/Slim/GUI/ControlPanel.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::MainFrame; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/GUI/ControlPanel/Account.pm b/Slim/GUI/ControlPanel/Account.pm index 2779f969923..116a3bb61fc 100644 --- a/Slim/GUI/ControlPanel/Account.pm +++ b/Slim/GUI/ControlPanel/Account.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::Account; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/GUI/ControlPanel/Advanced.pm b/Slim/GUI/ControlPanel/Advanced.pm index 969789dc82f..f157a0f9e1d 100644 --- a/Slim/GUI/ControlPanel/Advanced.pm +++ b/Slim/GUI/ControlPanel/Advanced.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::Advanced; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/GUI/ControlPanel/Diagnostics.pm b/Slim/GUI/ControlPanel/Diagnostics.pm index fc3c3828d9f..3bb78b2ebbd 100644 --- a/Slim/GUI/ControlPanel/Diagnostics.pm +++ b/Slim/GUI/ControlPanel/Diagnostics.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::Diagnostics; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/GUI/ControlPanel/InitialSettings.pm b/Slim/GUI/ControlPanel/InitialSettings.pm index 1f786f6f6b4..08c9f9578ae 100644 --- a/Slim/GUI/ControlPanel/InitialSettings.pm +++ b/Slim/GUI/ControlPanel/InitialSettings.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::InitialSettings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/GUI/ControlPanel/Music.pm b/Slim/GUI/ControlPanel/Music.pm index 8770341f950..14742de597e 100644 --- a/Slim/GUI/ControlPanel/Music.pm +++ b/Slim/GUI/ControlPanel/Music.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::Music; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/GUI/ControlPanel/Settings.pm b/Slim/GUI/ControlPanel/Settings.pm index 42fc58afef8..de3ba302eff 100644 --- a/Slim/GUI/ControlPanel/Settings.pm +++ b/Slim/GUI/ControlPanel/Settings.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/GUI/ControlPanel/Status.pm b/Slim/GUI/ControlPanel/Status.pm index f5d68dda1d5..8936673395c 100644 --- a/Slim/GUI/ControlPanel/Status.pm +++ b/Slim/GUI/ControlPanel/Status.pm @@ -1,6 +1,6 @@ package Slim::GUI::ControlPanel::Status; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Hardware/BacklightLED.pm b/Slim/Hardware/BacklightLED.pm index 6dc0338a894..8daf5c1d50e 100644 --- a/Slim/Hardware/BacklightLED.pm +++ b/Slim/Hardware/BacklightLED.pm @@ -1,6 +1,6 @@ package Slim::Hardware::BacklightLED; -# Logitech Media Server Copyright (c) 2001-2011 Logitech. +# Logitech Media Server Copyright (c) 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Hardware/IR.pm b/Slim/Hardware/IR.pm index 4e10c9b446a..91b5ab64065 100644 --- a/Slim/Hardware/IR.pm +++ b/Slim/Hardware/IR.pm @@ -1,8 +1,7 @@ package Slim::Hardware::IR; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Hardware/TriLED.pm b/Slim/Hardware/TriLED.pm index 20370652d14..c0393015b97 100644 --- a/Slim/Hardware/TriLED.pm +++ b/Slim/Hardware/TriLED.pm @@ -1,6 +1,6 @@ package Slim::Hardware::TriLED; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Hardware/mas3507d.pm b/Slim/Hardware/mas3507d.pm index cb50789e810..2d61ead344f 100644 --- a/Slim/Hardware/mas3507d.pm +++ b/Slim/Hardware/mas3507d.pm @@ -1,8 +1,7 @@ package Slim::Hardware::mas3507d; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Hardware/mas35x9.pm b/Slim/Hardware/mas35x9.pm index f712011b97d..7fde78fa6d9 100644 --- a/Slim/Hardware/mas35x9.pm +++ b/Slim/Hardware/mas35x9.pm @@ -1,8 +1,7 @@ package Slim::Hardware::mas35x9; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Media/MediaFolderScan.pm b/Slim/Media/MediaFolderScan.pm index a0e300b2361..062246d4cf5 100644 --- a/Slim/Media/MediaFolderScan.pm +++ b/Slim/Media/MediaFolderScan.pm @@ -1,8 +1,6 @@ package Slim::Media::MediaFolderScan; -# $Id -# -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Menu/AlbumInfo.pm b/Slim/Menu/AlbumInfo.pm index aa554a60c52..d1505461a47 100644 --- a/Slim/Menu/AlbumInfo.pm +++ b/Slim/Menu/AlbumInfo.pm @@ -1,6 +1,6 @@ package Slim::Menu::AlbumInfo; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -510,7 +510,7 @@ sub cliQuery { my $menuMode = $request->getParam('menu') || 0; my $menuContext = $request->getParam('context') || 'normal'; my $playlist_index = defined( $request->getParam('playlist_index') ) ? $request->getParam('playlist_index') : undef; - my $connectionId = $request->connectionID; + my $connectionId = $request->connectionID || ''; my %filter; foreach (qw(artist_id genre_id year library_id)) { diff --git a/Slim/Menu/ArtistInfo.pm b/Slim/Menu/ArtistInfo.pm index e6e56e6b9f5..7452a710a79 100644 --- a/Slim/Menu/ArtistInfo.pm +++ b/Slim/Menu/ArtistInfo.pm @@ -1,6 +1,6 @@ package Slim::Menu::ArtistInfo; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -295,7 +295,7 @@ sub cliQuery { my $menuMode = $request->getParam('menu') || 0; my $menuContext = $request->getParam('context') || 'normal'; my $playlist_index = defined( $request->getParam('playlist_index') ) ? $request->getParam('playlist_index') : undef; - my $connectionId = $request->connectionID; + my $connectionId = $request->connectionID || ''; my $tags = { menuMode => $menuMode, diff --git a/Slim/Menu/Base.pm b/Slim/Menu/Base.pm index 7647a3ddeda..c59f1b4e35a 100644 --- a/Slim/Menu/Base.pm +++ b/Slim/Menu/Base.pm @@ -1,8 +1,6 @@ package Slim::Menu::Base; -# $Id: $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Menu/BrowseLibrary.pm b/Slim/Menu/BrowseLibrary.pm index df40a443442..48fe8c0b2a0 100644 --- a/Slim/Menu/BrowseLibrary.pm +++ b/Slim/Menu/BrowseLibrary.pm @@ -633,7 +633,9 @@ sub _registerBaseNodes { feed => \&_bmf, icon => 'html/images/musicfolder.png', homeMenuText => 'BROWSE_MUSIC_FOLDER', - condition => sub {return isEnabledNode(@_) && scalar @{ Slim::Utils::Misc::getAudioDirs() };}, + condition => sub { + return isEnabledNode(@_) && (scalar @{ Slim::Utils::Misc::getAudioDirs() } || scalar @{ Slim::Utils::Misc::getInactiveMediaDirs() }); + }, id => 'myMusicMusicFolder', weight => 70, cache => 0, # don't cache BMF modes, as it should act on the latest disk content! @@ -644,6 +646,7 @@ sub _registerBaseNodes { params => {mode => 'playlists'}, feed => \&_playlists, icon => 'html/images/playlists.png', + homeMenuText => 'SAVED_PLAYLISTS', condition => sub { return unless isEnabledNode(@_); return 1 if Slim::Utils::Misc::getPlaylistDir(); @@ -822,7 +825,7 @@ sub _generic { # remote_library might be part of the @searchTags. But it's to be consumed by # BrowseLibrary, rather than by the CLI. if (!$args->{remote_library}) { - ($args->{remote_library}) = map { /remote_library:(.*)/ && $1 } grep /remote_library/, @$queryTags; + ($args->{remote_library}) = map { /remote_library:(.*)/ && $1 } grep { $_ && /remote_library/ } @$queryTags; } # library_id:-1 is supposed to clear/override the global library_id @@ -1974,7 +1977,7 @@ sub _bmf { my $remote_library = $args->{'remote_library'} ||= $pt->{'remote_library'}; my @searchTags = $pt->{'searchTags'} ? @{$pt->{'searchTags'}} : (); - _generic($client, $callback, $args, 'musicfolder', ['tags:cdus', @searchTags], + _generic($client, $callback, $args, 'musicfolder', ['tags:cdus' . ($remote_library ? 'o' : ''), @searchTags], sub { my $results = shift; my $gotsubfolder = 0; diff --git a/Slim/Menu/FolderInfo.pm b/Slim/Menu/FolderInfo.pm index 954baaa5b91..fc7901a785d 100644 --- a/Slim/Menu/FolderInfo.pm +++ b/Slim/Menu/FolderInfo.pm @@ -1,6 +1,6 @@ package Slim::Menu::FolderInfo; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -168,7 +168,7 @@ sub cliQuery { my $client = $request->client; my $folder_id = $request->getParam('folder_id'); my $menuMode = $request->getParam('menu') || 0; - my $connectionId = $request->connectionID; + my $connectionId = $request->connectionID || ''; unless ( $folder_id || $cachedFeed{$connectionId} ) { $request->setStatusBadParams(); diff --git a/Slim/Menu/GenreInfo.pm b/Slim/Menu/GenreInfo.pm index 2295f1037c1..5576994ed6e 100644 --- a/Slim/Menu/GenreInfo.pm +++ b/Slim/Menu/GenreInfo.pm @@ -1,6 +1,6 @@ package Slim::Menu::GenreInfo; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Menu/GlobalSearch.pm b/Slim/Menu/GlobalSearch.pm index 10ec29347d9..579a7d9e716 100644 --- a/Slim/Menu/GlobalSearch.pm +++ b/Slim/Menu/GlobalSearch.pm @@ -1,6 +1,6 @@ package Slim::Menu::GlobalSearch; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Menu/PlaylistInfo.pm b/Slim/Menu/PlaylistInfo.pm index fc5ff1d1050..985a3eaafb4 100644 --- a/Slim/Menu/PlaylistInfo.pm +++ b/Slim/Menu/PlaylistInfo.pm @@ -1,6 +1,6 @@ package Slim::Menu::PlaylistInfo; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Menu/SystemInfo.pm b/Slim/Menu/SystemInfo.pm index 398d31bd0a1..9d8ba00ed02 100644 --- a/Slim/Menu/SystemInfo.pm +++ b/Slim/Menu/SystemInfo.pm @@ -1,8 +1,6 @@ package Slim::Menu::SystemInfo; -# $Id: $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -37,7 +35,7 @@ my $prefs = preferences('server'); sub init { my $class = shift; $class->SUPER::init(); - + Slim::Control::Request::addDispatch( [ 'systeminfo', 'items', '_index', '_quantity' ], [ 0, 1, 1, \&cliQuery ] @@ -67,27 +65,27 @@ sub registerDefaultInfoProviders { after => 'server', func => \&infoLibrary, ) ); - + $class->registerInfoProvider( currentplayer => ( after => 'library', func => \&infoCurrentPlayer, ) ); - + $class->registerInfoProvider( players => ( after => 'currentplayer', func => \&infoPlayers, ) ); - + $class->registerInfoProvider( dirs => ( after => 'players', func => \&infoDirs, ) ); - + $class->registerInfoProvider( logs => ( after => 'dirs', func => \&infoLogs, ) ); - + $class->registerInfoProvider( plugins => ( after => 'logs', func => \&infoPlugins, @@ -97,54 +95,54 @@ sub registerDefaultInfoProviders { sub infoPlayers { my $client = shift; my $tags = shift; - + my @players = Slim::Player::Client::clients(); return {} if ! scalar @players; - + my $item = { name => cstring($client, 'INFORMATION_MENU_PLAYER'), items => [] }; - + for my $player (sort { $a->name cmp $b->name } @players) { - + my ($raw, $details) = _getPlayerInfo($player, $tags); - + push @{ $item->{items} }, { name => $player->name, items => $details, web => { name => $player->name, items => $raw, - } + } } } - + return $item; } sub infoCurrentPlayer { my $client = shift || return; my $tags = shift; - + my ($raw, $details) = _getPlayerInfo($client, $tags); - + my $item = { name => cstring($client, 'INFORMATION_SPECIFIC_PLAYER', $client->name), items => $details, web => { hide => 1, items => $raw, - } + } }; - + return $item; } sub _getPlayerInfo { my $client = shift; my $tags = shift; - + my $info = [ # { INFORMATION_PLAYER_NAME_ABBR => $client->name }, { INFORMATION_PLAYER_MODEL => $client->modelName }, @@ -160,13 +158,13 @@ sub _getPlayerInfo { my @details; foreach (@$info) { my ($key, $value) = each %{$_}; - + next unless defined $value; - + if (Slim::Utils::Strings::stringExists($key . '_ABBR')) { $key = $key . '_ABBR' } - + push @details, { type => 'text', name => cstring($client, $key) . cstring($client, 'COLON') . ' ' . $value, @@ -174,18 +172,18 @@ sub _getPlayerInfo { } if (Slim::Utils::PluginManager->isEnabled('Slim::Plugin::Health::Plugin')) { - + if (my $netinfo = Slim::Plugin::Health::NetTest::systemInfoMenu($client, $tags)) { push @details, $netinfo; } } - + return ($info, \@details); } sub infoLibrary { my $client = shift; - + if (!Slim::Schema::hasLibrary()) { return { name => cstring($client, 'INFORMATION_MENU_LIBRARY'), @@ -197,19 +195,19 @@ sub infoLibrary { ], }; } - + elsif (Slim::Music::Import->stillScanning) { return { name => cstring($client, 'RESCANNING_SHORT'), - + web => { hide => 1, }, - } + } } - + my $totals = Slim::Schema->totals(); - + my $items = { name => cstring($client, 'INFORMATION_MENU_LIBRARY'), @@ -254,23 +252,23 @@ sub infoLibrary { # don't bother counting images/videos unless media are enabled if ( main::MEDIASUPPORT ) { my ($request, $results); - + # XXX - no simple access to result sets for images/videos yet? if (main::VIDEO) { $request = Slim::Control::Request::executeRequest( $client, ['video_titles', 0, 0] ); $results = $request->getResults(); - + unshift @{ $items->{items} }, { type => 'text', name => cstring($client, 'INFORMATION_VIDEOS') . cstring($client, 'COLON') . ' ' . ($results && $results->{count} ? Slim::Utils::Misc::delimitThousands($results->{count}) : 0), }; } - + if (main::IMAGE) { $request = Slim::Control::Request::executeRequest( $client, ['image_titles', 0, 0] ); $results = $request->getResults(); - + unshift @{ $items->{items} }, { type => 'text', name => cstring($client, 'INFORMATION_IMAGES') . cstring($client, 'COLON') . ' ' @@ -278,47 +276,48 @@ sub infoLibrary { }; } } - + return $items; } sub infoServer { my $client = shift; my $tags = shift; - + my $menu = $tags->{menuMode}; - + my $osDetails = Slim::Utils::OSDetect::details(); - + my $items = [ { type => 'text', - name => sprintf("Logitech Media Server %s%s %s - %s @ %s", + name => sprintf("%s %s%s %s - %s @ %s", + cstring($client, 'SQUEEZEBOX_SERVER'), cstring($client, 'INFORMATION_VERSION'), cstring($client, 'COLON'), $::VERSION, $::REVISION, $::BUILDDATE), }, - + { type => 'text', name => cstring($client, 'INFORMATION_HOSTNAME') . cstring($client, 'COLON') . ' ' . Slim::Utils::Network::hostName(), - }, - + }, + { type => 'text', name => cstring($client, 'INFORMATION_SERVER_IP' . ($menu ? '_ABBR' : '')) . cstring($client, 'COLON') . ' ' . Slim::Utils::Network::serverAddr(), - }, - + }, + { type => 'text', name => cstring($client, 'INFORMATION_SERVER_HTTP' . ($menu ? '_ABBR' : '')) . cstring($client, 'COLON') . ' ' . $prefs->get('httpport'), - }, - + }, + { type => 'text', name => sprintf("%s%s %s - %s - %s ", @@ -328,26 +327,30 @@ sub infoServer { $prefs->get('language'), Slim::Utils::Unicode::currentLocale()), }, - + { type => 'text', name => cstring($client, 'INFORMATION_ARCHITECTURE' . ($menu ? '_ABBR' : '')) . cstring($client, 'COLON') . ' ' . ($osDetails->{'osArch'} ? $osDetails->{'osArch'} : 'unknown'), }, - + { type => 'text', name => cstring($client, 'PERL_VERSION') . cstring($client, 'COLON') . ' ' . $Config{'version'} . ' - ' . $Config{'archname'}, }, - - # XXX - let's show the Audio::Scan version until we've updated them all + { type => 'text', name => 'Audio::Scan' . cstring($client, 'COLON') . ' ' . $Audio::Scan::VERSION, }, + + { + type => 'text', + name => 'IO::Socket::SSL' . cstring($client, 'COLON') . ' ' . (Slim::Networking::Async::HTTP->hasSSL() ? $IO::Socket::SSL::VERSION : cstring($client, 'BLANK')), + }, ]; - + if ( Slim::Schema::hasLibrary() ) { push @{$items}, { type => 'text', @@ -355,7 +358,7 @@ sub infoServer { . Slim::Utils::OSDetect->getOS->sqlHelperClass->sqlVersionLong( Slim::Schema->dbh ), }; } - + push @{$items}, { type => 'text', name => cstring($client, 'INFORMATION_CLIENTS') . cstring($client, 'COLON') . ' ' @@ -370,7 +373,7 @@ sub infoServer { sub infoDirs { my $client = shift; - + my $folders = [ { INFORMATION_CACHEDIR => $prefs->get('cachedir') }, { INFORMATION_PREFSDIR => Slim::Utils::Prefs::dir() }, @@ -378,17 +381,17 @@ sub infoDirs { { INFORMATION_PLUGINDIRS => join(", ", grep {$_ !~ m|Slim/Plugin|} Slim::Utils::OSDetect::dirsFor('Plugins')) }, { INFORMATION_BINDIRS => join(", ", Slim::Utils::Misc::getBinPaths()) }, ]; - + my $item = { name => cstring($client, 'FOLDERS'), items => [], - + web => { name => 'FOLDERS', items => $folders, } }; - + foreach (@$folders) { my ($key, $value) = each %{$_}; push @{ $item->{items} }, { @@ -396,21 +399,21 @@ sub infoDirs { name => cstring($client, $key) . cstring($client, 'COLON') . ' ' . $value, } } - + return $item; } sub infoPlugins { my $client = shift || return; my $tags = shift; - + my $item = { name => cstring($client, 'INFORMATION_MENU_MODULE'), items => [], - + web => { hide => 1, - } + } }; my $plugins = Slim::Utils::PluginManager->allPlugins; @@ -426,7 +429,7 @@ sub infoPlugins { my $name = $plugins->{$plugin}->{'name'}; my $version = $plugins->{$plugin}->{'version'}; - + if ( $name eq uc($name) ) { $name = cstring($client, $name); } @@ -441,43 +444,43 @@ sub infoPlugins { @list = sort { $a->{'name'} cmp $b->{'name'} } @list; $item->{'items'} = \@list; - + return $item; } sub infoLogs { my $client = shift; - + my $logs = Slim::Utils::Log->getLogFiles(); - + my $item = { name => cstring($client, 'SETUP_DEBUG_SERVER_LOG'), items => [], - + web => { name => 'SETUP_DEBUG_SERVER_LOG', items => $logs, } }; - + foreach (@$logs) { my ($key, $value) = each %{$_}; - + next unless $value; - + push @{ $item->{items} }, { type => 'text', name => cstring($client, "SETUP_DEBUG_${key}_LOG") . cstring($client, 'COLON') . ' ' . $value } } - + return $item; } sub cliQuery { my $request = shift; - + my $client = $request->client; my $tags = { menuMode => $request->getParam('menu') || 0, diff --git a/Slim/Menu/TrackInfo.pm b/Slim/Menu/TrackInfo.pm index 399b4d0b66e..817d48cb677 100644 --- a/Slim/Menu/TrackInfo.pm +++ b/Slim/Menu/TrackInfo.pm @@ -1,8 +1,7 @@ package Slim::Menu::TrackInfo; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -188,9 +187,21 @@ sub registerDefaultInfoProviders { func => \&infoFileSize, ) ); - $class->registerInfoProvider( url => ( + $class->registerInfoProvider( playcount => ( parent => 'moreinfo', after => 'filesize', + func => \&infoPlayCount, + ) ); + + $class->registerInfoProvider( lastplayed => ( + parent => 'moreinfo', + after => 'playcount', + func => \&infoLastPlayed, + ) ); + + $class->registerInfoProvider( url => ( + parent => 'moreinfo', + after => 'lastplayed', func => \&infoUrl, ) ); @@ -952,19 +963,19 @@ sub infoReplayGain { my $album = $track->album; if ( my $replaygain = $track->replay_gain ) { - push @{$items}, _replainGainItem($client, $replaygain, $track->replay_peak, 'REPLAYGAIN'); + push @{$items}, _replayGainItem($client, $replaygain, $track->replay_peak, 'REPLAYGAIN'); } if ( blessed($album) && $album->can('replay_gain') ) { if ( my $albumreplaygain = $album->replay_gain ) { - push @{$items}, _replainGainItem($client, $albumreplaygain, $album->replay_peak, 'ALBUMREPLAYGAIN'); + push @{$items}, _replayGainItem($client, $albumreplaygain, $album->replay_peak, 'ALBUMREPLAYGAIN'); } } return $items; } -sub _replainGainItem { +sub _replayGainItem { my ($client, $replaygain, $replaygainpeak, $tag) = @_; my $noclip = Slim::Player::ReplayGain::preventClipping( $replaygain, $replaygainpeak ); @@ -1096,6 +1107,38 @@ sub infoFileSize { return $item; } +sub infoPlayCount { + my ( $client, $url, $track ) = @_; + + my $item; + + if ( my $count = $track->playcount ) { + $item = { + type => 'text', + label => 'PLAYCOUNT', + name => Slim::Utils::Misc::delimitThousands($count), + }; + } + + return $item; +} + +sub infoLastPlayed { + my ( $client, $url, $track ) = @_; + + my $item; + + if ( my $lastplayed = $track->lastplayed ) { + $item = { + type => 'text', + label => 'LASTPLAYED', + name => $track->buildModificationTime($lastplayed), + }; + } + + return $item; +} + sub infoRemoteTitle { my ( $client, $url, $track, $remoteMeta ) = @_; diff --git a/Slim/Menu/YearInfo.pm b/Slim/Menu/YearInfo.pm index cfa5454212f..3c7a6782121 100644 --- a/Slim/Menu/YearInfo.pm +++ b/Slim/Menu/YearInfo.pm @@ -1,6 +1,6 @@ package Slim::Menu::YearInfo; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index 3686a34d70d..988bcf7b120 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -1,8 +1,7 @@ package Slim::Music::Artwork; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -56,20 +55,20 @@ my %findArtCache; # Public class methods sub findStandaloneArtwork { my ( $class, $trackAttributes, $deferredAttributes, $dirurl ) = @_; - + my $isInfo = main::INFOLOG && $log->is_info; - + my $art = $findArtCache{$dirurl}; - + # Files to look for my @files = qw(cover folder album thumb); - + # User-defined artwork format my $coverFormat = $prefs->get('coverArt'); if ( !defined $art ) { my $parentDir = Path::Class::dir( Slim::Utils::Misc::pathFromFileURL($dirurl) ); - + # coverArt/artfolder pref support if ( $coverFormat ) { # If the user has specified a pattern to match the artwork on, we need @@ -80,7 +79,7 @@ sub findStandaloneArtwork { # Merge attributes to use with TitleFormatter # XXX This may break for some people as it's not using a Track object anymore my $meta = { %{$trackAttributes}, %{$deferredAttributes} }; - + if ( my $prefix = Slim::Music::TitleFormatter::infoFormat( undef, $1, undef, $meta ) ) { $coverFormat = $prefix . $suffix; @@ -88,18 +87,18 @@ sub findStandaloneArtwork { # Remove illegal characters from filename. $coverFormat =~ s/\\|\/|\:|\*|\?|\"|<|>|\|//g; } - + # Generating a pathname from tags is dangerous because the filesystem # encoding may not match the locale, but that is the best guess that we have. $coverFormat = Slim::Utils::Unicode::encode_locale($coverFormat); my $artPath = $parentDir->file($coverFormat)->stringify; - + if ( my $artDir = $prefs->get('artfolder') ) { $artDir = Path::Class::dir($artDir); $artPath = $artDir->file($coverFormat)->stringify; } - + if ( -e $artPath ) { $isInfo && $log->info("Found variable cover $coverFormat from $1"); $art = $artPath; @@ -121,21 +120,21 @@ sub findStandaloneArtwork { push @files, $coverFormat; } } - + if ( !$art ) { # Find all image files in the file directory my $types = qr/\.(?:jpe?g|png|gif)$/i; - + my $files = File::Next::files( { file_filter => sub { Slim::Utils::Misc::fileFilter($File::Next::dir, $_, $types, undef, 1) }, descend_filter => sub { 0 }, }, $parentDir ); - + my @found; while ( my $image = $files->() ) { push @found, $image; } - + # Prefer cover/folder/album/thumb, then just take the first image my $filelist = join( '|', @files ); if ( my @preferred = grep { basename($_) =~ qr/^(?:$filelist)\./i } @found ) { @@ -145,7 +144,7 @@ sub findStandaloneArtwork { $art = $found[0] || 0; } } - + # Cache found artwork for this directory to speed up later tracks # No caching if using a user-defined artwork format, the user may have multiple # files in a single directory with different artwork @@ -154,18 +153,36 @@ sub findStandaloneArtwork { $findArtCache{$dirurl} = $art; } } - + $isInfo && $log->info("Using $art"); - + return $art || 0; } sub updateStandaloneArtwork { my $class = shift; my $cb = shift; # optional callback when done (main process async mode) - + my $dbh = Slim::Schema->dbh; - + + my $where = qq{ + tracks.cover LIKE '%jpg' + OR tracks.cover LIKE '%jpeg' + OR tracks.cover LIKE '%png' + OR tracks.cover LIKE '%gif' + OR tracks.coverid IS NULL + }; + + # get singledir parameter from the scanner if available + my $singledir = main::SCANNER ? $ARGV[-1] : undef; + if ($singledir) { + $singledir = Slim::Utils::Misc::fileURLFromPath(Slim::Utils::Unicode::encode_locale($singledir)); + $where = qq{ + url LIKE '$singledir%' + AND ($where) + }; + } + # Find all tracks with un-cached artwork: # * All distinct cover values where cover isn't 0 and cover_cached is null # * Tracks share the same cover art when the cover field is the same @@ -181,7 +198,7 @@ sub updateStandaloneArtwork { albums.artwork AS album_artwork FROM tracks JOIN albums ON (tracks.album = albums.id) - WHERE tracks.cover LIKE '%jpg' OR tracks.cover LIKE '%jpeg' OR tracks.cover LIKE '%png' OR tracks.cover LIKE '%gif' OR tracks.coverid IS NULL + WHERE $where GROUP BY tracks.cover, tracks.album }; @@ -190,7 +207,7 @@ sub updateStandaloneArtwork { SET cover = ?, coverid = ?, cover_cached = NULL WHERE album = ? } ); - + my $sth_update_albums = $dbh->prepare( qq{ UPDATE albums SET artwork = ? @@ -200,37 +217,37 @@ sub updateStandaloneArtwork { my ($count) = $dbh->selectrow_array( qq{ SELECT COUNT(*) FROM ( $sql ) AS t1 } ); - + $log->error("Starting updateStandaloneArtwork for $count albums"); - + if ( !$count ) { $cb && $cb->(); main::SCANNER && Slim::Music::Import->endImporter('updateStandaloneArtwork'); return; } - my $progress = Slim::Utils::Progress->new( { + my $progress = Slim::Utils::Progress->new( { type => 'importer', name => 'updateStandaloneArtwork', - total => $count, + total => $count, bar => 1, } ); - + my $sth = $dbh->prepare($sql); $sth->execute; - + my ($trackid, $url, $cover, $coverid, $albumid, $album_title, $album_artwork); $sth->bind_columns(\$trackid, \$url, \$cover, \$coverid, \$albumid, \$album_title, \$album_artwork); - + my $i = 0; my $t = 0; - + my $work = sub { if ( $sth->fetch ) { my $newCoverId; - + $progress->update( $album_title ); - + if ( $t < time ) { Slim::Schema->forceCommit; $t = time + 5; @@ -243,26 +260,26 @@ sub updateStandaloneArtwork { url => $url, }); } - + # check for new artwork to unchanged file # - !$cover: there wasn't any previously # - !$newCoverId: existing file has disappeared if ( !$cover || !$newCoverId ) { # store properties in a hash my $track = Slim::Schema->find('Track', $trackid); - + if ($track) { my %columnValueHash = map { $_ => $track->$_() } keys %{$track->attributes}; $columnValueHash{primary_artist} = $columnValueHash{primary_artist}->id if $columnValueHash{primary_artist}; my $newCover = Slim::Music::Artwork->findStandaloneArtwork( \%columnValueHash, - {}, + {}, Slim::Utils::Misc::fileURLFromPath( dirname(Slim::Utils::Misc::pathFromFileURL($url)) ), ); - + if ($newCover) { $cover = $newCover; @@ -273,7 +290,7 @@ sub updateStandaloneArtwork { } } } - + if ( $newCoverId && ($coverid || '') ne $newCoverId ) { # Make sure album.artwork points to this track, as it may not # be pointing there now because we did not join tracks via the @@ -281,7 +298,7 @@ sub updateStandaloneArtwork { if ( ($album_artwork || '') ne $newCoverId ) { $sth_update_albums->execute( $newCoverId, $albumid ); } - + # Update the rest of the tracks on this album # to use the same coverid and cover_cached status $sth_update_tracks->execute( $cover, $newCoverId, $albumid ); @@ -290,7 +307,7 @@ sub updateStandaloneArtwork { Slim::Schema->forceCommit; $t = time + 5; } - + Slim::Utils::Scheduler::unpause() if !main::SCANNER; } # cover art has disappeared @@ -300,29 +317,29 @@ sub updateStandaloneArtwork { $log->warn('Artwork has been removed for ' . $album_title); } - + return 1; } - + $progress->final; - + $log->error( "updateStandaloneArtwork finished in " . $progress->duration ); - + $cb && $cb->(); - + return 0; }; - + if ( main::SCANNER ) { # Non-async mode in scanner while ( $work->() ) { } - + Slim::Music::Import->endImporter('updateStandaloneArtwork'); } else { # Run async in main process Slim::Utils::Scheduler::add_ordered_task($work); - } + } } sub getImageContentAndType { @@ -348,7 +365,7 @@ sub getImageContentAndType { sub readCoverArt { my $class = shift; - my $track = shift; + my $track = shift; my $url = Slim::Utils::Misc::stripAnchorFromURL($track->url); my $file = $track->path; @@ -431,7 +448,7 @@ sub _readCoverArtTags { if ($body) { my $contentType = $class->_imageContentType(\$body); - + $isInfo && $log->info(sprintf("Found image of length [%d] bytes with type: [$contentType]", length($body))); return ($body, $contentType, length($body)); @@ -449,7 +466,7 @@ sub _readCoverArtFiles { my $class = shift; my $track = shift; my $path = shift; - + my $isInfo = main::INFOLOG && $log->is_info; my @names = qw(cover Cover thumb Thumb album Album folder Folder); @@ -462,7 +479,7 @@ sub _readCoverArtFiles { $isInfo && $log->info("Looking for image files in $parentDir"); my %nameslist = map { $_ => [do { my $t = $_; map { "$t.$_" } @ext }] } @names; - + # these seem to be in a particular order - not sure if that means anything. my @filestotry = map { @{$nameslist{$_}} } @names; my $artwork = $prefs->get('coverArt'); @@ -475,41 +492,41 @@ sub _readCoverArtFiles { if (my $prefix = Slim::Music::TitleFormatter::infoFormat( Slim::Utils::Misc::fileURLFromPath($track->url), $1)) { - + $artwork = $prefix . $suffix; - + $isInfo && $log->info("Variable cover: $artwork from $1"); - + if (main::ISWINDOWS) { # Remove illegal characters from filename. $artwork =~ s/\\|\/|\:|\*|\?|\"|<|>|\|//g; - } - + } + # Generating a pathname from tags is dangerous because the filesystem # encoding may not match the locale, but that is the best guess that we have. $artwork = Slim::Utils::Unicode::encode_locale($artwork); - + my $artPath = $parentDir->file($artwork)->stringify; - + my ($body, $contentType) = $class->getImageContentAndType($artPath); - + my $artDir = dir($prefs->get('artfolder')); - + if (!$body && defined $artDir) { - + $artPath = $artDir->file($artwork)->stringify; - + ($body, $contentType) = $class->getImageContentAndType($artPath); } - + if ($body && $contentType) { - + $isInfo && $log->info("Found image file: $artPath"); - + return ($body, $contentType, $artPath); } } else { - + $isInfo && $log->info("Variable cover: no match from $1"); } @@ -570,13 +587,13 @@ sub precacheAllArtwork { my $class = shift; my $cb = shift; # optional callback when done (main process async mode) my $force = shift; # sometimes we want all artwork to be re-rendered - + my $isDebug = main::DEBUGLOG && $importlog->is_debug; - + my $isEnabled = $prefs->get('precacheArtwork'); - + my $dbh = Slim::Schema->dbh; - + # Find all tracks with un-cached artwork: # * All distinct cover values where cover isn't 0 and cover_cached is null # * Tracks share the same cover art when the cover field is the same @@ -595,7 +612,7 @@ sub precacheAllArtwork { AND tracks.coverid IS NOT NULL } . ($force ? '' : ' AND tracks.cover_cached IS NULL') - . qq{ + . qq{ GROUP BY tracks.cover }; @@ -605,7 +622,7 @@ sub precacheAllArtwork { WHERE album = ? AND cover = ? } ); - + my $sth_update_albums = $dbh->prepare( qq{ UPDATE albums SET artwork = ? @@ -615,50 +632,50 @@ sub precacheAllArtwork { my ($count) = $dbh->selectrow_array( qq{ SELECT COUNT(*) FROM ( $sql ) AS t1 } ); - + $log->error("Starting precacheArtwork for $count albums"); - + if ( !$count ) { $cb && $cb->(); - + if ( main::SCANNER ) { Slim::Music::Import->endImporter('precacheArtwork'); } # wipe internal cache - %findArtCache = (); + %findArtCache = (); return; } - my $progress = Slim::Utils::Progress->new( { + my $progress = Slim::Utils::Progress->new( { type => 'importer', name => 'precacheArtwork', - total => $count, + total => $count, bar => 1, } ); - + # Pre-cache this artwork resized to our commonly-used sizes/formats # 1. user's thumb size or 100x100_o (large web artwork) # 2. 50x50_o (small web artwork) # 3+ SqueezePlay/Jive size artwork my @specs; - + if ($isEnabled) { @specs = getResizeSpecs(); - + require Slim::Utils::ImageResizer; } - + my $sth = $dbh->prepare($sql); $sth->execute; - + my ($url, $cover, $coverid, $albumid, $album_title, $album_artwork); $sth->bind_columns(\$url, \$cover, \$coverid, \$albumid, \$album_title, \$album_artwork); - + my $i = 0; - + my %artCount; - + my $work = sub { if ( $sth->fetch ) { # Make sure album.artwork points to this track, as it may not @@ -667,47 +684,47 @@ sub precacheAllArtwork { if ( $album_artwork && $album_artwork ne $coverid ) { $sth_update_albums->execute( $coverid, $albumid ); } - + $artCount{$albumid}++; - + # Callback after resize is finished, needed for async resizing - my $finished = sub { + my $finished = sub { if ($isEnabled) { # Update the rest of the tracks on this album # to use the same coverid and cover_cached status $sth_update_tracks->execute( $coverid, $albumid, $cover ); } - + $progress->update( $album_title ); if ( ++$i % 50 == 0 ) { Slim::Schema->forceCommit; } - + Slim::Utils::Scheduler::unpause() if !main::SCANNER; }; - + # Do the actual pre-caching only if the pref for it is enabled if ( $isEnabled ) { # Image to resize is either a cover path or the audio file my $path = $cover =~ /^\d+$/ ? Slim::Utils::Misc::pathFromFileURL($url) : $cover; - + $isDebug && $importlog->debug( "Pre-caching artwork for " . $album_title . " from $path" ); - + # have scheduler wait for the finished callback Slim::Utils::Scheduler::pause() if !main::SCANNER; - + Slim::Utils::ImageResizer->resize($path, "music/$coverid/cover_", join(',', @specs), $finished); } else { $finished->(); } - + return 1; } - + # for albums where we have different track artwork, use the first track's cover as the album artwork my $sth_get_album_art = $dbh->prepare_cached( qq{ SELECT tracks.coverid @@ -717,46 +734,46 @@ sub precacheAllArtwork { ORDER BY tracks.disc, tracks.tracknum LIMIT 1 }); - + $i = 0; while ( my ($albumId, $trackCount) = each %artCount ) { - + next unless $trackCount > 1; $sth_get_album_art->execute($albumId); my ($coverId) = $sth_get_album_art->fetchrow_array; - + $sth_update_albums->execute( $coverId, $albumId ) if $coverId; - + } %artCount = (); - + $progress->final; - + $log->error( "precacheArtwork finished in " . $progress->duration ); - + $cb && $cb->(); $sth_get_album_art->finish; # wipe internal cache - %findArtCache = (); - + %findArtCache = (); + return 0; }; - + if ( main::SCANNER ) { # Non-async mode in scanner while ( $work->() ) { } - + Slim::Music::Import->endImporter('precacheArtwork'); } else { # Run async in main process Slim::Utils::Scheduler::add_ordered_task($work); - } + } } sub getResizeSpecs { @@ -766,35 +783,39 @@ sub getResizeSpecs { '40x40_m', # Fab4 Album list ); - if (!Slim::Utils::OSDetect::isSqueezeOS()) { - my $thumbSize = $prefs->get('thumbSize') || 100; - - push(@specs, - "${thumbSize}x${thumbSize}_o", # Web UI large thumbnails - '50x50_o', # Web UI small thumbnails, Controller App (low-res display) - ); - - if ( my $customSpecs = $prefs->get('customArtSpecs') ) { - main::DEBUGLOG && $log->is_debug && $log->debug("Adding custom artwork resizing specs:\n" . Data::Dump::dump($customSpecs)); - push @specs, keys %$customSpecs; - } - - # sort by size, so we can batch convert - @specs = sort { - my ($sizeA) = $a =~ /^(\d+)/; - my ($sizeB) = $b =~ /^(\d+)/; - $b <=> $a; - # XXX - this is duplicated from Slim::Web::Graphics->parseSpec, which is not loaded in scanner mode - } grep { - /^(?:([0-9X]+)x([0-9X]+))?(?:_(\w))?(?:_([\da-fA-F]+))?(?:\.(\w+))?$/ - # remove duplicates - } keys %{{ - map {$_ => 1} @specs - }}; - - main::DEBUGLOG && $log->is_debug && $log->debug("Full list of artwork pre-cache specs:\n" . Data::Dump::dump(@specs)); + my $thumbSize = $prefs->get('thumbSize') || 100; + + push(@specs, + "${thumbSize}x${thumbSize}_o", # Web UI large thumbnails + '50x50_o', # Web UI small thumbnails, Controller App (low-res display) + ); + + # HiDPI versions of web UI artwork + if ($prefs->get('precacheHiDPIArtwork')) { + $thumbSize *= 2; + push @specs, "${thumbSize}x${thumbSize}_o"; + } + + if ( my $customSpecs = $prefs->get('customArtSpecs') ) { + main::DEBUGLOG && $log->is_debug && $log->debug("Adding custom artwork resizing specs:\n" . Data::Dump::dump($customSpecs)); + push @specs, keys %$customSpecs; } - + + # sort by size, so we can batch convert + @specs = sort { + my ($sizeA) = $a =~ /^(\d+)/; + my ($sizeB) = $b =~ /^(\d+)/; + $b <=> $a; + # XXX - this is duplicated from Slim::Web::Graphics->parseSpec, which is not loaded in scanner mode + } grep { + /^(?:([0-9X]+)x([0-9X]+))?(?:_(\w))?(?:_([\da-fA-F]+))?(?:\.(\w+))?$/ + # remove duplicates + } keys %{{ + map {$_ => 1} @specs + }}; + + main::DEBUGLOG && $log->is_debug && $log->debug("Full list of artwork pre-cache specs:\n" . Data::Dump::dump(@specs)); + return @specs; } diff --git a/Slim/Music/Import.pm b/Slim/Music/Import.pm index db2004fca28..91c54d9c8c8 100644 --- a/Slim/Music/Import.pm +++ b/Slim/Music/Import.pm @@ -1,6 +1,6 @@ package Slim::Music::Import; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Music/Info.pm b/Slim/Music/Info.pm index 534e6a42c25..f1182e71f5c 100644 --- a/Slim/Music/Info.pm +++ b/Slim/Music/Info.pm @@ -1,8 +1,7 @@ package Slim::Music::Info; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -1487,10 +1486,6 @@ sub typeFromPath { } elsif ($fullpath =~ /^([a-z]+:)/ && defined($suffixes{$1})) { $type = $suffixes{$1}; - } - elsif ( $fullpath =~ /^(?:live365)/ ) { - # Force mp3 for protocol handlers - return 'mp3'; } else { diff --git a/Slim/Music/PlaylistFolderScan.pm b/Slim/Music/PlaylistFolderScan.pm index 86fe387c0bf..4ba69cb7d9e 100644 --- a/Slim/Music/PlaylistFolderScan.pm +++ b/Slim/Music/PlaylistFolderScan.pm @@ -1,8 +1,6 @@ package Slim::Music::PlaylistFolderScan; -# $Id -# -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Music/TitleFormatter.pm b/Slim/Music/TitleFormatter.pm index 4057d269853..c2ecb301b16 100644 --- a/Slim/Music/TitleFormatter.pm +++ b/Slim/Music/TitleFormatter.pm @@ -1,8 +1,7 @@ package Slim::Music::TitleFormatter; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -38,7 +37,7 @@ sub init { # for relating track attributes to album/artist attributes my @trackAttrs = (); - + require Slim::Schema::Track; # Subs for all regular track attributes @@ -49,42 +48,46 @@ sub init { if ( ref $_[0] eq 'HASH' ) { return $_[0]->{ lc($attr) } || $_[0]->{ 'tracks.' . lc($attr) } || ''; } - + my $output = $_[0]->get_column($attr); return (defined $output ? $output : ''); }; } - + # localize content type where possible $parsedFormats{'CT'} = sub { my $output = $parsedFormats{'CONTENT_TYPE'}->(@_); - + if (!$output && ref $_[0] eq 'HASH' ) { $output = $_[0]->{ct} || $_[0]->{ 'tracks.ct' } || ''; } - + $output = Slim::Utils::Strings::getString( uc($output) ) if $output; - + + if (!$output && ref $_[0] eq 'HASH' ) { + $output = $_[0]->{type} || ''; + } + return $output; }; # Override album $parsedFormats{'ALBUM'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{album} || $_[0]->{'albums.title'} || ''; } my $output = ''; $output = $_[0]->albumname(); - $output = '' if $output eq string('NO_ALBUM'); + $output = '' if !defined($output) || $output eq string('NO_ALBUM'); return (defined $output ? $output : ''); }; # add album related $parsedFormats{'ALBUMSORT'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{albumsort} || $_[0]->{'albums.titlesort'} || ''; } @@ -100,7 +103,7 @@ sub init { }; $parsedFormats{'DISCC'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{discc} || $_[0]->{'albums.discc'} || ''; } @@ -116,7 +119,7 @@ sub init { }; $parsedFormats{'DISC'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{disc} || $_[0]->{'tracks.disc'} || ''; } @@ -124,7 +127,7 @@ sub init { my $disc = $_[0]->disc; if ($disc && $disc == 1) { - + my $albumDiscc_sth = Slim::Schema->dbh->prepare_cached("SELECT discc FROM albums WHERE id = ?"); $albumDiscc_sth->execute($_[0]->albumid); @@ -143,7 +146,7 @@ sub init { # add artist related $parsedFormats{'ARTIST'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{artist} || $_[0]->{albumartist} || $_[0]->{trackartist} || $_[0]->{'contributors.name'} || ''; } @@ -158,7 +161,7 @@ sub init { push @output, $name; } - + # Bug 12162: cope with objects that only have artistName and no artists if (!(scalar @output) && $_[0]->can('artistName')) { my $name = $_[0]->artistName(); @@ -171,7 +174,7 @@ sub init { }; $parsedFormats{'ARTISTSORT'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{artistsort} || $_[0]->{'contributors.titlesort'} || ''; } @@ -195,16 +198,16 @@ sub init { for my $attr (qw(composer conductor band)) { $parsedFormats{uc($attr)} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{$attr} || ''; } my $output = ''; - + eval { my ($item) = $_[0]->$attr(); - + if ($item) { $output = $item->name(); } @@ -216,7 +219,7 @@ sub init { # add genre $parsedFormats{'GENRE'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{genre} || $_[0]->{'genres.name'} || ''; } @@ -246,7 +249,7 @@ sub init { $parsedFormats{'DURATION'} = sub { if ( ref $_[0] eq 'HASH' ) { my $duration = $_[0]->{duration} || $_[0]->{'tracks.duration'} || $_[0]->{'secs'} || ''; - + # format if we got a number only return sprintf('%s:%02s', int($duration / 60), $duration % 60) if $duration * 1 eq $duration; return $duration; @@ -272,10 +275,10 @@ sub init { return Slim::Music::Info::getCurrentBitrate($_[0]->url) || $_[0]->prettyBitRate; }; - + # add file info $parsedFormats{'VOLUME'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{volume} || ''; } @@ -296,7 +299,7 @@ sub init { }; $parsedFormats{'PATH'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{path} || ''; } @@ -317,7 +320,7 @@ sub init { }; $parsedFormats{'FILE'} = sub { - + my $url; if ( ref $_[0] eq 'HASH' ) { if ( $_[0]->{url} ) { @@ -347,11 +350,11 @@ sub init { }; $parsedFormats{'EXT'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{ext} || ''; } - + my $output = ''; my $url = $_[0]->get('url'); @@ -370,17 +373,17 @@ sub init { # Add date/time elements $parsedFormats{'LONGDATE'} = sub { - return Slim::Utils::DateTime::longDateF(); + return Slim::Utils::DateTime::longDateF(); }; - + $parsedFormats{'SHORTDATE'} = sub { return Slim::Utils::DateTime::shortDateF(); }; - + $parsedFormats{'CURRTIME'} = sub { return Slim::Utils::DateTime::timeF(); }; - + # Add localized from/by $parsedFormats{'FROM'} = sub { return string('FROM'); }; $parsedFormats{'BY'} = sub { return string('BY'); }; @@ -396,7 +399,7 @@ sub init { # Add lightweight FILE.EXT format $parsedFormats{'FILE.EXT'} = sub { - + if ( ref $_[0] eq 'HASH' ) { return $_[0]->{'file.ext'} || ''; } @@ -433,7 +436,7 @@ sub addFormat { my ($package) = caller(); $externalFormats->{$format}++ if $package !~ /^Slim/; - + # only add format if it is not already defined if (!defined $parsedFormats{$format}) { @@ -552,9 +555,9 @@ sub _parseFormat { # break up format string into separators and elements # elements must be separated by non-word characters @parsed = ($format =~ m/(.*?)\b($elemRegex)\b/gc); - + # add anything remaining at the end - # perl 5.6 doesn't like retaining the pos() on m//gc in list context, + # perl 5.6 doesn't like retaining the pos() on m//gc in list context, # so use the length of the joined matches to determine where we left off push @parsed, substr($format,length(join '', @parsed)); @@ -638,12 +641,12 @@ sub infoFormat { my $meta = shift; # optional metadata hash to use instead of object data my $output = ''; my $format; - + # use a safe format string if none specified # Bug: 1146 - Users can input strings in any locale - we need to convert that to # UTF-8 first, otherwise perl will segfault in the nasty regex below. if ($str && $] > 5.007) { - + my $old = $str; if ( !($str = $formatCache{$old}) ) { $str = $old; @@ -662,7 +665,7 @@ sub infoFormat { # Get the formatting function from the hash, or parse it $format = $parsedFormats{$str} || _parseFormat($str); - + # Short-circuit if we have metadata if ( $meta ) { # Make sure all keys in meta are lowercase for format lookups @@ -670,7 +673,7 @@ sub infoFormat { for my $key ( @uckeys ) { $meta->{lc($key)} = $meta->{$key}; } - + $output = $format->($meta) if ref($format) eq 'CODE'; } else { @@ -692,7 +695,7 @@ sub infoFormat { $output = $format->($track) if ref($format) eq 'CODE'; } - + $output = '' if !defined $output; if ($output eq "" && defined($safestr)) { diff --git a/Slim/Music/VirtualLibraries.pm b/Slim/Music/VirtualLibraries.pm index 19250113b32..5ef5cc45e36 100644 --- a/Slim/Music/VirtualLibraries.pm +++ b/Slim/Music/VirtualLibraries.pm @@ -1,6 +1,6 @@ package Slim::Music::VirtualLibraries; -# Logitech Media Server Copyright 2001-2014 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Async.pm b/Slim/Networking/Async.pm index 94efd7046f4..118bb16b72a 100644 --- a/Slim/Networking/Async.pm +++ b/Slim/Networking/Async.pm @@ -1,8 +1,7 @@ package Slim::Networking::Async; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -16,6 +15,7 @@ use base qw(Slim::Utils::Accessor); use Scalar::Util qw(blessed weaken); use Socket qw(inet_ntoa); +use Errno qw(EWOULDBLOCK EAGAIN); use Slim::Networking::Async::DNS; use Slim::Networking::Async::Socket::HTTP; @@ -111,6 +111,7 @@ sub connect { # Bug 5673, avoid a crash if socket is undef if ( !defined $socket ) { + $log->error("Failed to connect to $host:$port, because\n$@"); _connect_error( $socket, $self, $args ); return; } @@ -144,7 +145,7 @@ sub _connect_error { # Kill the timeout timer Slim::Utils::Timers::killTimers( $socket, \&_connect_error ); - + # close the socket if ( defined $socket ) { $socket->close; @@ -163,14 +164,28 @@ sub _connect_error { sub _async_connect { my ( $socket, $self, $args ) = @_; - - # Kill the timeout timer - Slim::Utils::Timers::killTimers( $socket, \&_connect_error ); + + # on Windows $! might not be cleared when socket is not yet connected + # and whatever was previous value is kept, making the test below fail + # by clearing it, we force underlying to redefine it. + $! = undef; # check that we are actually connected if ( !$socket->connected ) { - return _connect_error( $socket, $self, $args ); + if ($socket->isa('Slim::Networking::Async::Socket::HTTPS') && ($! == EWOULDBLOCK || $! == EAGAIN)) { + # The TLS handshake is not yet complete. Retry later. + return; + } + else { + # remove our initial selects + Slim::Networking::Select::removeError($socket); + Slim::Networking::Select::removeWrite($socket); + return _connect_error( $socket, $self, $args ); + } } + + # Kill the timeout timer + Slim::Utils::Timers::killTimers( $socket, \&_connect_error ); # remove our initial selects Slim::Networking::Select::removeError($socket); diff --git a/Slim/Networking/Async/DNS.pm b/Slim/Networking/Async/DNS.pm index 65be2de6aba..0483b6b60ec 100644 --- a/Slim/Networking/Async/DNS.pm +++ b/Slim/Networking/Async/DNS.pm @@ -1,8 +1,7 @@ package Slim::Networking::Async::DNS; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Async/HTTP.pm b/Slim/Networking/Async/HTTP.pm index 1825d61086a..8e5542ddb14 100644 --- a/Slim/Networking/Async/HTTP.pm +++ b/Slim/Networking/Async/HTTP.pm @@ -1,31 +1,36 @@ package Slim::Networking::Async::HTTP; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. # This class provides an async HTTP implementation. use strict; +use constant MIN_IO_SOCKET_SSL => '2.020'; + BEGIN { my $hasSSL; sub hasSSL { return $hasSSL if defined $hasSSL; - + $hasSSL = 0; - eval { + eval { require Slim::Networking::Async::Socket::HTTPS; $hasSSL = 1; }; if ($@) { - msg("Async::HTTP: Unable to load IO::Socket::SSL, will try connecting to SSL servers in non-SSL mode\n"); + msg("Async::HTTP: Unable to load IO::Socket::SSL, will try connecting to SSL servers in non-SSL mode\n$@\n"); + } + # if we're using an outdated version of IO::Socket::SSL, log a warning + elsif (Slim::Utils::Versions->compareVersions(MIN_IO_SOCKET_SSL, $IO::Socket::SSL::VERSION) > 0) { + msg("You're using a rather old version of IO::Socket::SSL (v$IO::Socket::SSL::VERSION) - please try to update to at least " . MIN_IO_SOCKET_SSL . " for improved compatibility.\n"); } return $hasSSL; @@ -47,6 +52,7 @@ use Slim::Utils::Log; use Slim::Utils::Misc; use Slim::Utils::Prefs; use Slim::Utils::Timers; +use Slim::Utils::Versions; use constant BUFSIZE => 16 * 1024; use constant MAX_REDIR => 7; @@ -58,47 +64,105 @@ my $cookieJar; my $log = logger('network.asynchttp'); __PACKAGE__->mk_accessor( rw => qw( - uri request response saveAs fh timeout maxRedirect + uri request response saveAs fh timeout maxRedirect socks ) ); sub init { $cookieJar = HTTP::Cookies->new( file => catdir($prefs->get('cachedir'), 'cookies.dat'), autosave => 1 ); } +sub new { + my ($class, $args) = @_; + my $self = $class->SUPER::new; + + if ( $args->{socks} ) { + eval { + require Slim::Networking::Async::Socket::HTTPSocks; + require Slim::Networking::Async::Socket::HTTPSSocks if hasSSL(); + }; + + if (!$@) { + # no need for a hash mk_accessor type as we don't access individual keys + $self->socks($args->{socks}); + main::INFOLOG && $log->info("Using SOCKS $args->{ProxyAddr}::$args->{ProxyPort} to connect"); + } + } + + return $self; +} + sub new_socket { my $self = shift; - + if ( my $proxy = $self->use_proxy ) { main::INFOLOG && $log->info("Using proxy $proxy to connect"); - + my ($pserver, $pport) = split /:/, $proxy; - + return Slim::Networking::Async::Socket::HTTP->new( @_, PeerAddr => $pserver, PeerPort => $pport || 80, ); } - + # Create SSL socket if URI is https if ( $self->request->uri->scheme eq 'https' ) { if ( hasSSL() ) { - return Slim::Networking::Async::Socket::HTTPS->new( @_ ); + # From http://bugs.slimdevices.com/show_bug.cgi?id=18152: + # We increasingly find servers *requiring* use of the SNI extension to TLS. + # IO::Socket::SSL supports this and, in combination with the Net:HTTPS 'front-end', + # will default to using a server name (PeerAddr || Host || PeerHost). But this will fail + # if PeerAddr has been set to, say, IPv4 or IPv6 address form. And LMS does that through + # DNS lookup. + # So we will probably need to explicitly set "SSL_hostname" if we are to succeed with such + # a server. + + # First, try without explicit SNI, so we don't inadvertently break anything. + # (This is the 'old' behaviour.) (Probably overly conservative.) + + my $sock; + + if ($self->socks) { + $sock = Slim::Networking::Async::Socket::HTTPSSocks->new( %{$self->socks}, @_ ); + } + else { + $sock = Slim::Networking::Async::Socket::HTTPS->new( @_ ); + } + return $sock if $sock; + + my %args = @_; + + # Failed. Try again with an explicit SNI. + $args{SSL_hostname} = $args{Host}; + $args{SSL_verify_mode} = Net::SSLeay::VERIFY_NONE(); + if ($self->socks) { + return Slim::Networking::Async::Socket::HTTPSSocks->new( %{$self->socks}, %args ); + } + else { + return Slim::Networking::Async::Socket::HTTPS->new( %args ); + } } else { # change the request to port 80 $self->request->uri->scheme( 'http' ); $self->request->uri->port( 80 ); - + my %args = @_; $args{PeerPort} = 80; - - $log->warn("Warning: trying HTTP request to HTTPS server"); - - return Slim::Networking::Async::Socket::HTTP->new( %args ); + + if ($self->socks) { + return Slim::Networking::Async::Socket::HTTPSocks->new( %{$self->socks}, %args ); + } + else { + return Slim::Networking::Async::Socket::HTTP->new( %args ); + } } } + elsif ($self->socks) { + return Slim::Networking::Async::Socket::HTTPSocks->new( %{$self->socks}, @_ ); + } else { return Slim::Networking::Async::Socket::HTTP->new( @_ ); } @@ -106,7 +170,7 @@ sub new_socket { sub use_proxy { my $self = shift; - + # Proxy will be used for non-local HTTP requests if ( my $proxy = $prefs->get('webproxy') ) { my $host = $self->request->uri->host; @@ -115,44 +179,45 @@ sub use_proxy { return $proxy; } } - + return; } sub send_request { my ( $self, $args ) = @_; - + $self->maxRedirect( $args->{maxRedirect} || MAX_REDIR ); - + if ( $args->{Timeout} ) { $self->timeout( $args->{Timeout} ); } - + # option to save directly to a file if ( $args->{saveAs} ) { $self->saveAs( $args->{saveAs} ); } - - $self->request( + + $self->request( $args->{request} || HTTP::Request->new( $args->{method} => $args->{url} ) ); - + if ( $self->request->uri !~ /^https?:/i ) { my $error = 'Cannot request non-HTTP URL ' . $self->request->uri; return $self->_http_error( $error, $args ); } - + if ( !$self->request->protocol ) { $self->request->protocol( 'HTTP/1.0' ); } - + + # the used class is NET::HTTP::Method which now supports HTTP 1.1 # XXX until we support chunked encoding, force 1.0 - $self->request->protocol('HTTP/1.0'); - + # $self->request->protocol('HTTP/1.0'); + $self->add_headers(); - + $self->write_async( { host => $self->request->uri->host, port => $self->request->uri->port, @@ -168,27 +233,32 @@ sub send_request { # add standard request headers sub add_headers { my $self = shift; - + my $headers = $self->request->headers; - + # handle basic auth if username, password provided if ( my $userinfo = $self->request->uri->userinfo ) { $headers->header( Authorization => 'Basic ' . encode_base64( $userinfo, '' ) ); } - + my $host = $self->request->uri->host; - if ( $self->request->uri->port != 80 ) { + # http://bugs.slimdevices.com/show_bug.cgi?id=18151 + # Host needs port number unless we're using default (http - 80, https - 443). + # Note that both curl & wget suppress the default port number. Source comment in wget suggests + # that broken server-side software often doesn't recognize the port argument. + if ( $self->request->uri->port != $self->request->uri->default_port ) { $host .= ':' . $self->request->uri->port; } # Host doesn't use init_header so it will be changed if we're redirecting $headers->header( Host => $host ); - + $headers->init_header( 'User-Agent' => Slim::Utils::Misc::userAgentString() ); $headers->init_header( Accept => '*/*' ); $headers->init_header( 'Cache-Control' => 'no-cache' ); - $headers->init_header( Connection => 'close' ); - + # only init 'Connection' header if HTTP is 1.0, otherwise leave if to Net::HTTP::Method + $headers->init_header( Connection => 'close' ) if $self->request->protocol =~ m|HTTP/1.0|i; + if ( $headers->header('User-Agent') !~ /^NSPlayer/ ) { $headers->init_header( 'Icy-Metadata' => 1 ); } @@ -206,15 +276,15 @@ sub cookie_jar { sub _format_request { my $self = shift; - + my $fullpath = $self->request->uri->path_query; $fullpath = "/$fullpath" unless $fullpath =~ /^\//; - + # Proxy requests require full URL if ( $self->use_proxy ) { $fullpath = $self->request->uri->as_string; } - + my @h; $self->request->headers->scan( sub { my ($k, $v) = @_; @@ -222,22 +292,26 @@ sub _format_request { $v =~ s/\n/ /g; push @h, $k, $v; } ); - + # Add POST body if any my $content_ref = $self->request->content_ref; if ( ref $content_ref ) { push @h, $$content_ref; } - - # XXX until we support chunked encoding, force 1.0 - $self->socket->http_version('1.0'); + + # Support HTTP 1.1 and keep-alive + my ($version) = $self->request->protocol =~ m|HTTP/(\S*)|i; + $version = '1.0' if $version ne '1.1'; + $self->socket->http_version($version); + $self->socket->keep_alive(1) if ($version == '1.1' && $self->request->header('Connection') !~ /close/i) || + $self->request->header('Connection') =~ /keep-alive/i; my $request = $self->socket->format_request( $self->request->method, $fullpath, @h, ); - + return \$request; } @@ -248,31 +322,47 @@ sub read_body { my $args = shift; $self->socket->set( passthrough => [ $self, $args ] ); - + Slim::Networking::Select::addError( $self->socket, \&_http_socket_error ); Slim::Networking::Select::addRead( $self->socket, \&_http_read_body ); } +sub suspend_stream { + my $self = shift; + + Slim::Utils::Timers::killTimers( $self->socket, \&_http_read_timeout ); + Slim::Networking::Select::removeRead( $self->socket ); +} + +sub resume_stream { + my $self = shift; + + my $timeout = $self->timeout || $prefs->get('remotestreamtimeout'); + + Slim::Utils::Timers::setTimer( $self->socket, Time::HiRes::time() + $timeout, \&_http_read_timeout, $self->socket->get('passthrough') ); + Slim::Networking::Select::addRead( $self->socket, \&_http_read_body ); +} + sub _http_socket_error { my ( $socket, $self, $args ) = @_; - + Slim::Utils::Timers::killTimers( $socket, \&_http_socket_error ); - + $self->disconnect; - + return $self->_http_error( "Error on HTTP socket: $!", $args ); } sub _http_error { my ( $self, $error, $args ) = @_; - + if ( $self->fh ) { $self->fh->close; } - + $self->disconnect; - # Bug 8801, Only print an error if the caller doesn't have an onError handler + # Bug 8801, Only print an error if the caller doesn't have an onError handler if ( my $ecb = $args->{onError} ) { my $passthrough = $args->{passthrough} || []; $ecb->( $self, $error, @{$passthrough} ); @@ -284,19 +374,28 @@ sub _http_error { sub _http_read { my ( $self, $args ) = @_; - + my ($code, $mess, @h) = eval { $self->socket->read_response_headers }; - + + # XXX - this is a hack to work around some mis-configured streaming services. + # Net::HTTP::Methods would reject status which don't start with HTTP/. + # Some streaming services return "ICY 200 OK" instead. Let's deal with + # them in a more relaxed way and give them another try. + if ($@ && $@ =~ /ICY 200 OK/) { + ($code, $mess, @h) = eval { $self->socket->read_response_headers( laxed => 1 ) }; + @h = $self->socket->_read_header_lines() if !$@ && $code == 200 && $mess =~ /OK/ && !scalar @h; + } + if ($@) { $self->_http_error( "Error reading headers: $@", $args ); return; } - + if ($code) { # headers complete, remove ourselves from select loop Slim::Networking::Select::removeError( $self->socket ); Slim::Networking::Select::removeRead( $self->socket ); - + # do we have a previous response from a redirect? my $previous = []; if ( $self->response ) { @@ -305,112 +404,125 @@ sub _http_read { } push @{$previous}, $self->response->clone; } - + my $headers = HTTP::Headers->new; while ( @h ) { my ($k, $v) = splice @h, 0, 2; $headers->push_header( $k => $v ); } $self->response( HTTP::Response->new( $code, $mess, $headers ) ); - + # Save previous response $self->response->previous( $previous ); - + $self->response->request( $self->request ); # Save cookies $cookieJar->extract_cookies( $self->response ); - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug("Headers read. code: $code status: $mess"); $log->debug( Data::Dump::dump( $self->response->headers ) ); } - + if ( $code !~ /[23]\d\d/ ) { return $self->_http_error( $self->response->status_line, $args ); } - + # Handle redirects if ( $code =~ /^30[1237]$/ ) { my $location = $self->response->header('Location'); - + # check max redirects if ( $location && scalar @{$previous} < $self->maxRedirect ) { - + $self->disconnect; - + # change the request object to the new location delete $args->{request}; $self->request->uri( URI->new_abs( $location, $self->request->uri ) ); - + if ( main::INFOLOG && $log->is_info ) { $log->info(sprintf("Redirecting to %s", $self->request->uri->as_string)); } - + # Does the caller want to modify redirecting URLs? if ( $args->{onRedirect} ) { my $passthrough = $args->{passthrough} || []; $args->{onRedirect}->( $self->request, @{$passthrough} ); } - + $self->send_request( { request => $self->request, %{$args}, } ); - + return; } else { + my $error = 'Redirection without location'; - my $error = $location ? 'Redirection limit exceeded' : 'Redirection without location'; + if ($location) { + $error = ($location =~ /^https/ && !hasSSL()) ? "Can't connect to https URL lack of IO::Socket::SSL: $location" : 'Redirection limit exceeded'; + } $log->warn($error); - + $self->disconnect; - + if ( my $cb = $args->{onError} ) { my $passthrough = $args->{passthrough} || []; return $cb->( $self, $error, @{$passthrough} ); } - + return; } } - + # Does the caller want a callback on headers? if ( my $cb = $args->{onHeaders} ) { my $passthrough = $args->{passthrough} || []; return $cb->( $self, @{$passthrough} ); } - + # if not, keep going and read the body $self->socket->set( passthrough => [ $self, $args ] ); - + # Timer in case the server never sends any body data my $timeout = $self->timeout || $prefs->get('remotestreamtimeout'); Slim::Utils::Timers::setTimer( $self->socket, Time::HiRes::time() + $timeout, \&_http_socket_error, $self, $args ); - + Slim::Networking::Select::addError( $self->socket, \&_http_socket_error ); Slim::Networking::Select::addRead( $self->socket, \&_http_read_body ); + + # in a *confirmed* keep-alive situation, the whole body might already be in the buffer, + # so there could be no further event which would lead to an error timeout, so body reading + # must be forced. But if nothing has been read yet, attempting to force body reading can lead to + # a false empty body result, so let it to the event loop. + # Body might also be empty but a keep-alive with no content-length in the response is an error + # if everything has already been read, _http_body_read will unsubscribe to event loop + # we just subscrive above ... a bit unefficient + _http_read_body( $self->socket, $self, $args ) if ( $self->response->headers->header('Connection') =~ /keep-alive/i && $self->socket->_rbuf_length == ($headers->content_length || 0)); } } sub _http_read_body { my ( $socket, $self, $args ) = @_; + my $result = $socket->read_entity_body( my $buf, BUFSIZE ); + return if $result < 0; + Slim::Utils::Timers::killTimers( $socket, \&_http_socket_error ); Slim::Utils::Timers::killTimers( $socket, \&_http_read_timeout ); - - my $result = $socket->read_entity_body( my $buf, BUFSIZE ); if ( $result ) { main::DEBUGLOG && $log->debug("Read body: [$result] bytes"); } - + # Are we saving directly to a file? if ( $result && $self->saveAs && !$self->fh ) { open my $fh, '>', $self->saveAs or do { @@ -418,14 +530,14 @@ sub _http_read_body { }; binmode $fh; - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug("Writing response directly to " . $self->saveAs); } - + $self->fh( $fh ); } - + if ( $result && $self->saveAs ) { # Write directly to a file $self->fh->write( $buf, length $buf ) or do { @@ -435,8 +547,11 @@ sub _http_read_body { elsif ( $args->{onStream} ) { # The caller wants a callback on every chunk of data streamed my $pt = $args->{passthrough} || []; + if ( !$result ) { + $buf = defined $result ? "" : undef; + } my $more = $args->{onStream}->( $self, \$buf, @{$pt} ); - + # onStream callback can signal to stop the stream by returning false if ( !$more ) { $result = 0; @@ -446,32 +561,40 @@ sub _http_read_body { # Add buffer to Response object $self->response->add_content( $buf ); } - + # Does the caller want us to quit reading early (i.e. for mp3 frames)? if ( $args->{readLimit} && length( $self->response->content ) >= $args->{readLimit} ) { - + # close and remove the socket $self->disconnect; - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug(sprintf("Body read (stopped after %d bytes)", length( $self->response->content ))); } - + if ( my $cb = $args->{onBody} ) { my $passthrough = $args->{passthrough} || []; return $cb->( $self, @{$passthrough} ); } } - - if ( !defined $result || $result == 0 ) { + + if ( !defined $result || $result == 0 || (defined $self->response->headers->header('Content-Length') && length($self->response->content) == $self->response->headers->header('Content-Length')) ) { # if here, we've reached the end of the body - - # close and remove the socket - $self->fh->close if $self->fh; - $self->disconnect; - + + # close and remove the socket if not keep-alive + if ( $self->response->headers->header('Connection') =~ /close/i || $self->request->headers->header('Connection') !~ /keep-alive/i ) { + $self->fh->close if $self->fh; + $self->disconnect; + main::DEBUGLOG && $log->debug("closing mode"); + } + else { + Slim::Networking::Select::removeError( $self->socket ); + Slim::Networking::Select::removeRead( $self->socket ); + main::DEBUGLOG && $log->debug("keep-alive mode"); + } + main::DEBUGLOG && $log->debug("Body read"); - + if ( my $cb = $args->{onBody} ) { my $passthrough = $args->{passthrough} || []; $cb->( $self, @{$passthrough} ); @@ -479,7 +602,7 @@ sub _http_read_body { } else { # More body data to read - + # Some servers may never send EOF, but we want to return whatever data we've read my $timeout = $self->timeout || $prefs->get('remotestreamtimeout'); Slim::Utils::Timers::setTimer( $socket, Time::HiRes::time() + $timeout, \&_http_read_timeout, $self, $args ); @@ -488,16 +611,16 @@ sub _http_read_body { sub _http_read_timeout { my ( $socket, $self, $args ) = @_; - + $log->warn("Timed out waiting for more body data, returning what we have"); - + Slim::Networking::Select::removeError( $socket ); Slim::Networking::Select::removeRead( $socket ); - + # close and remove the socket $self->fh->close if $self->fh; $self->disconnect; - + if ( my $cb = $args->{onBody} ) { my $passthrough = $args->{passthrough} || []; $cb->( $self, @{$passthrough} ); diff --git a/Slim/Networking/Async/Socket.pm b/Slim/Networking/Async/Socket.pm index 1d78608e58a..abf38da0275 100644 --- a/Slim/Networking/Async/Socket.pm +++ b/Slim/Networking/Async/Socket.pm @@ -1,8 +1,7 @@ package Slim::Networking::Async::Socket; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Async/Socket/HTTP.pm b/Slim/Networking/Async/Socket/HTTP.pm index 6811af0e75b..4da72df36cd 100644 --- a/Slim/Networking/Async/Socket/HTTP.pm +++ b/Slim/Networking/Async/Socket/HTTP.pm @@ -1,8 +1,7 @@ package Slim::Networking::Async::Socket::HTTP; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Async/Socket/HTTPS.pm b/Slim/Networking/Async/Socket/HTTPS.pm index 00504af509d..f22b07b7bf9 100644 --- a/Slim/Networking/Async/Socket/HTTPS.pm +++ b/Slim/Networking/Async/Socket/HTTPS.pm @@ -1,8 +1,7 @@ package Slim::Networking::Async::Socket::HTTPS; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -14,7 +13,13 @@ BEGIN { use IO::Socket::SSL; } -use base qw(Net::HTTPS Slim::Networking::Async::Socket); +use base qw(Net::HTTPS::NB Slim::Networking::Async::Socket); + +sub new { + my ($class, %args) = @_; + $args{'Blocking'} = 0; + return $class->SUPER::new(%args); +} sub close { my $self = shift; @@ -28,4 +33,4 @@ sub close { $self->SUPER::close(); } -1; \ No newline at end of file +1; diff --git a/Slim/Networking/Async/Socket/HTTPSSocks.pm b/Slim/Networking/Async/Socket/HTTPSSocks.pm new file mode 100644 index 00000000000..de54b2a9ef3 --- /dev/null +++ b/Slim/Networking/Async/Socket/HTTPSSocks.pm @@ -0,0 +1,50 @@ +package Slim::Networking::Async::Socket::HTTPSSocks; + +use strict; +use IO::Socket::Socks; + +use base qw(IO::Socket::SSL IO::Socket::Socks Net::HTTP::Methods Slim::Networking::Async::Socket); + +use Slim::Networking::Async::Socket::HTTPS; + +sub new { + my ($class, %args) = @_; + + # gracefully downgrade to equivalent class w/o socks + return Slim::Networking::Async::Socket::HTTPS->new( %args ) unless $args{ProxyAddr}; + + my %params = ( + %args, + SocksVersion => $args{Username} ? 5 : 4, + AuthType => $args{Username} ? 'userpass' : 'none', + ConnectAddr => $args{PeerAddr} || $args{Host}, + ConnectPort => $args{PeerPort}, + Blocking => 1, + ); + + $params{ProxyPort} ||= 1080; + + # create the SOCKS object and connect + my $sock = IO::Socket::Socks->new(%params) || return; + $sock->blocking(0); + + # once connected SOCKS is a normal socket, so we can use start_SSL + IO::Socket::SSL->start_SSL($sock, @_); + + # as we inherit from IO::Socket::SSL, we can bless to our base class + bless $sock; +} + +sub close { + my $self = shift; + + # remove self from select loop + Slim::Networking::Select::removeError($self); + Slim::Networking::Select::removeRead($self); + Slim::Networking::Select::removeWrite($self); + Slim::Networking::Select::removeWriteNoBlockQ($self); + + $self->SUPER::close(); +} + +1; \ No newline at end of file diff --git a/Slim/Networking/Async/Socket/HTTPSocks.pm b/Slim/Networking/Async/Socket/HTTPSocks.pm new file mode 100644 index 00000000000..96343f99213 --- /dev/null +++ b/Slim/Networking/Async/Socket/HTTPSocks.pm @@ -0,0 +1,44 @@ +package Slim::Networking::Async::Socket::HTTPSocks; + +use strict; + +use base qw(IO::Socket::Socks Net::HTTP::Methods Slim::Networking::Async::Socket); + +use Slim::Networking::Async::Socket::HTTP; + +sub new { + my ($class, %args) = @_; + + # gracefully downgrade to equivalent class w/o socks + return Slim::Networking::Async::Socket::HTTP->new( %args ) unless $args{ProxyAddr}; + + my %params = ( + %args, + SocksVersion => $args{Username} ? 5 : 4, + AuthType => $args{Username} ? 'userpass' : 'none', + ConnectAddr => $args{PeerAddr} || $args{Host}, + ConnectPort => $args{PeerPort}, + Blocking => 1, + ); + + $params{ProxyPort} ||= 1080; + + my $sock = $class->SUPER::new(%params) || return; + $sock->blocking(0); + + bless $sock; +} + +sub close { + my $self = shift; + + # remove self from select loop + Slim::Networking::Select::removeError($self); + Slim::Networking::Select::removeRead($self); + Slim::Networking::Select::removeWrite($self); + Slim::Networking::Select::removeWriteNoBlockQ($self); + + $self->SUPER::close(); +} + +1; \ No newline at end of file diff --git a/Slim/Networking/Async/Socket/UDP.pm b/Slim/Networking/Async/Socket/UDP.pm index d0bad5206e3..35709aed44f 100644 --- a/Slim/Networking/Async/Socket/UDP.pm +++ b/Slim/Networking/Async/Socket/UDP.pm @@ -1,6 +1,6 @@ package Slim::Networking::Async::Socket::UDP; -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Discovery.pm b/Slim/Networking/Discovery.pm index 231f957f714..670d7e2a879 100644 --- a/Slim/Networking/Discovery.pm +++ b/Slim/Networking/Discovery.pm @@ -1,8 +1,7 @@ package Slim::Networking::Discovery; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Discovery/Players.pm b/Slim/Networking/Discovery/Players.pm index b501822c129..1d7c5046206 100644 --- a/Slim/Networking/Discovery/Players.pm +++ b/Slim/Networking/Discovery/Players.pm @@ -1,8 +1,6 @@ package Slim::Networking::Discovery::Players; -# $Id: Players.pm 18532 2008-04-07 23:10:43Z andy $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Discovery/Server.pm b/Slim/Networking/Discovery/Server.pm index 29d565dd314..ea9720a47d1 100644 --- a/Slim/Networking/Discovery/Server.pm +++ b/Slim/Networking/Discovery/Server.pm @@ -1,8 +1,6 @@ package Slim::Networking::Discovery::Server; -# $Id: Server.pm 15258 2007-12-13 15:29:14Z mherger $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/IO/Select.pm b/Slim/Networking/IO/Select.pm index a5a3c1bb39c..b493bc3302c 100644 --- a/Slim/Networking/IO/Select.pm +++ b/Slim/Networking/IO/Select.pm @@ -1,8 +1,7 @@ package Slim::Networking::IO::Select; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -102,7 +101,7 @@ sub _add { } } - return unless defined $fh; + return unless defined $fh && defined fileno($fh); my $w = EV::io( fileno($fh), diff --git a/Slim/Networking/Repositories.pm b/Slim/Networking/Repositories.pm index 97c3885786c..628fcbf9ab2 100644 --- a/Slim/Networking/Repositories.pm +++ b/Slim/Networking/Repositories.pm @@ -1,37 +1,37 @@ package Slim::Networking::Repositories; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. =head1 NAME -Slim::Music::Repositories +Slim::Networking::Repositories =head1 DESCRIPTION -Slim::Networking::Repositories provides some mechanisms to allow for a simple +Slim::Networking::Repositories provides some mechanisms to allow for a simple load balancing and failover etc. Callers can register multiple repositories, from which the fastest would be chosen automaically. -This module is doing a HEAD request on the URLs to measure latency. When a +This module is doing a HEAD request on the URLs to measure latency. When a URL for a repository is requested, the fastest will be returned. URLs with a latency within a certain threshold are considered on par with each other, and a random URL will be chosen. This module will not validate the URLs any further than trying to do a HEAD -request. Whether it's pointing to a file or a folder is up to the caller. -URLs which fail the latency check are filtered out. No check will be run if +request. Whether it's pointing to a file or a folder is up to the caller. +URLs which fail the latency check are filtered out. No check will be run if there is only one URL for a repository. PLEASE NOTE that the URLs need to be available for a HEAD request! If the URL is pointing to a folder which has no default document and where the directory -index is disabled will fail the latency check! In such a case please put a +index is disabled will fail the latency check! In such a case please put a minimalistic index.html in the folder. -Optionally there can be a repositories.conf with a list of repositories in -the same folder as strings.txt. This must not be writable by the server, as +Optionally there can be a repositories.conf with a list of repositories in +the same folder as strings.txt. This must not be writable by the server, as otherwise a malicious plugin could redirect update checks etc.! repositories.conf: @@ -41,7 +41,7 @@ servers http://downloads.myserver.com/respository.xml firmware http://downloads.myserver.com/firmware.xml =head1 METHODS - + # get the repository file from the "best" mirror: Slim::Networking::Repositories->get( 'servers', @@ -49,13 +49,13 @@ firmware http://downloads.myserver.com/firmware.xml \&_handleError, { ... }, # params passed through to the callbacks ); - + # ... or get the best URL to further deal with: Slim::Networking::Repositories->getUrlForRepository('servers'); - + # ... or get a mirror (or self) for a given URL: Slim::Networking::Repositories->getMirrorForUrl('http://downloads.myserver.com/repository.xml'); - + =cut use strict; @@ -68,11 +68,11 @@ use Slim::Utils::Timers; use constant POLL_INTERVAL => 3600 * 6; # mirrors whose latency is within this range (in seconds) will be picked randomly -use constant OK_THRESHOLD => 0.5; +use constant OK_THRESHOLD => 0.5; my $log = Slim::Utils::Log->addLogCategory( { category => 'network.repositories', - defaultLevel => 'ERROR', + defaultLevel => 'WARN', } ); my $prefs = preferences('server'); @@ -81,15 +81,17 @@ my $prefs = preferences('server'); # weighting. The default of 1 would be replaced with the latency in order to # allow latency based load balancing. my %repositories = ( - servers => { 'http://repos.squeezecommunity.org/' => 1 }, + servers => { 'https://downloads.slimdevices.com/releases/' => 1 }, firmware => { 'http://update.slimdevices.com/update/firmware/' => 1 }, - extensions => { 'http://repos.squeezecommunity.org/extensions.xml' => 1 }, + extensions => { 'https://github.com/LMS-Community/lms-plugin-repository/raw/master/extensions.xml' => 1 }, ); +my %missingSSLWarned; + sub init { # read optional file with additinal repositories my $reposfile = catfile(Slim::Utils::OSDetect::dirsFor('repositories'), 'repositories.conf'); - + if ( -f $reposfile && open(CONVERT, $reposfile) ) { while (my $line = ) { @@ -101,13 +103,26 @@ sub init { $line =~ s/#.*$//o; $line =~ s/^\s*//o; $line =~ s/\s*$//o; - + if ( $line =~ m|^([a-z_]+)\s+(https?://\S+)\s*$|i ) { $repositories{$1}->{$2} = 1; } } } - + + # use all plain text http if we lack https support + if (!Slim::Networking::Async::HTTP->hasSSL()) { + foreach my $category (keys %repositories) { + my $repos = delete $repositories{$category}; + + $repositories{$category} = { map { + my $url = $_; + $url =~ s/^https:/http:/; + $url => $repos->{$_}; + } keys %$repos }; + } + } + foreach (keys %repositories) { Slim::Utils::Timers::setTimer($_, time() + rand(5), \&measureLatency); } @@ -115,25 +130,45 @@ sub init { # get a repository file for a repository sub get { - my $class = shift; - my $item = shift; - + my ($class, $item, $cb, $ecb, $params) = @_; + my $url = $item =~ /^https?:/ ? $class->getMirrorForUrl($item) : $class->getUrlForRepository($item); - - Slim::Networking::SimpleAsyncHTTP->new( @_ )->get( $url ); + + if (!Slim::Networking::Async::HTTP->hasSSL() && $url =~ /^https:(.*)/ && !$missingSSLWarned{$url}++) { + $log->warn("Falling back to plain text http lack of IO::Socket::SSL: " . $url); + } + + $url =~ s/^https:/http:/ if $missingSSLWarned{$url}; + + Slim::Networking::SimpleAsyncHTTP->new($cb, sub { + my ($http, $error) = @_; + + my $url = $http->url; + + $log->error("Failed to fetch $url: $error"); + + if ($url =~ /^https:/) { + $log->warn("https lookup failed - trying plain text http instead: $url") unless $missingSSLWarned{$url}++; + $url =~ s/^(http)s:/$1:/; + Slim::Networking::SimpleAsyncHTTP->new($cb, $ecb, $params)->get($url); + } + else { + $ecb->($http, $error) + } + }, $params)->get( $url ); } sub getUrlForRepository { my ($class, $repository) = @_; - + return '' unless $repository; - + my @urls = keys %{ $repositories{$repository} || {} }; - + return '' unless scalar @urls; - + return $urls[0] if scalar @urls == 1; - + my $repositories = $repositories{$repository}; # filter out slow mirrors (difference to fastest larger than OK_THRESHOLD) @@ -141,31 +176,31 @@ sub getUrlForRepository { @urls = grep { $latency ||= $repositories->{$_}; $repositories->{$_} - $latency < OK_THRESHOLD ? 1 : 0; - } sort { + } sort { $repositories->{$a} <=> $repositories->{$b} } @urls; - + # pick random URL from remaining list my $url = $urls[ rand @urls ]; - + main::DEBUGLOG && $log->is_debug && $log->debug("Picked URL for repository '$repository': " . $url); - + return $url; } sub getMirrorForUrl { my ($class, $url) = @_; - + main::DEBUGLOG && $log->is_debug && $log->debug("Trying to find a mirror for URL: " . $url); - - my ($repository) = grep { - $repositories{$_}->{$url} ? $_ : undef + + my ($repository) = grep { + $repositories{$_}->{$url} ? $_ : undef } keys %repositories; - + if ($repository) { return $class->getUrlForRepository($repository); } - + return $url; } @@ -175,14 +210,14 @@ sub measureLatency { Slim::Utils::Timers::killTimers($repository, \&measureLatency); return unless keys %{$repositories{$repository}} > 1; - + for my $repo ( keys %{$repositories{$repository}} ) { Slim::Networking::SimpleAsyncHTTP->new( - \&_measureLatencyDone, - \&_measureLatencyDone, - { - repository => $repository, - sent => Time::HiRes::time(), + \&_measureLatencyDone, + \&_measureLatencyDone, + { + repository => $repository, + sent => Time::HiRes::time(), cache => 0, timeout => 5, } @@ -194,12 +229,12 @@ sub measureLatency { sub _measureLatencyDone { my $http = shift; - + my $code = $http->code || 0; my $url = $http->url; my $latency = Time::HiRes::time() - $http->params('sent'); - + if ( $code == 200 ) { main::DEBUGLOG && $log->is_debug && $log->debug("Got latency for $url: $latency"); } diff --git a/Slim/Networking/Select.pm b/Slim/Networking/Select.pm index 97a12ddfc7f..bf84becfef0 100644 --- a/Slim/Networking/Select.pm +++ b/Slim/Networking/Select.pm @@ -1,8 +1,7 @@ package Slim::Networking::Select; -# $Id$ -# Logitech Media Server Copyright 2003-2011 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/SimpleAsyncHTTP.pm b/Slim/Networking/SimpleAsyncHTTP.pm index 4404f76ed23..3ad2f2177d5 100644 --- a/Slim/Networking/SimpleAsyncHTTP.pm +++ b/Slim/Networking/SimpleAsyncHTTP.pm @@ -1,6 +1,6 @@ package Slim::Networking::SimpleAsyncHTTP; -# Logitech Media Server Copyright 2003-2016 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -201,7 +201,7 @@ sub _createHTTPRequest { } =cut - my $http = Slim::Networking::Async::HTTP->new; + my $http = Slim::Networking::Async::HTTP->new( $self->_params ); $http->send_request( { request => $request, maxRedirect => $params->{maxRedirect}, @@ -305,7 +305,7 @@ sub onBody { # size of the cache. We use ETag/Last Modified to check for stale data during # this time. my $max = 60 * 60 * 24; - my $expires; + my $expires; # undefined until max-age or expires header is seen, or caller defines it my $no_revalidate; if ( $params->{expires} ) { @@ -328,6 +328,15 @@ sub onBody { } } + # Don't cache if response is already stale, indicated by -ve expiry time. + # Remark: Caching with a negative expiry time is treated as "Never expire" by + # Slim::Utils::Cache/DbCache. Probably not what is wanted. + # Example seen: Server doesn't return Cache-Control header, but does return + # 'expires: Thu, 01 Jan 1970 00:00:00 GMT'. + if ( $expires && $expires < 0 ) { + $expires = 0; + } + # Don't cache for more than $max if ( $expires && $expires > $max ) { $expires = $max; @@ -346,7 +355,12 @@ sub onBody { } else { if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug(sprintf("Not caching [%s], no expiration set and missing cache headers", $self->url)); + if (defined $expires) { + $log->debug(sprintf("Not caching [%s], cache headers forbid, or apparently stale", $self->url)); + } + else { + $log->debug(sprintf("Not caching [%s], no expiration set and missing cache headers", $self->url)); + } } } } @@ -370,7 +384,7 @@ sub _cacheKey { my $cachekey = $url; if ($client) { - $cachekey .= '-' . $client->languageOverride; + $cachekey .= '-' . ($client->languageOverride || ''); } return $cachekey; diff --git a/Slim/Networking/SliMP3/Protocol.pm b/Slim/Networking/SliMP3/Protocol.pm index cd787709cd3..6bf6f829309 100644 --- a/Slim/Networking/SliMP3/Protocol.pm +++ b/Slim/Networking/SliMP3/Protocol.pm @@ -1,6 +1,6 @@ package Slim::Networking::SliMP3::Protocol; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/SliMP3/Stream.pm b/Slim/Networking/SliMP3/Stream.pm index 11f382fa9d1..96b5a93831e 100644 --- a/Slim/Networking/SliMP3/Stream.pm +++ b/Slim/Networking/SliMP3/Stream.pm @@ -1,8 +1,7 @@ package Slim::Networking::SliMP3::Stream; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/Slimproto.pm b/Slim/Networking/Slimproto.pm index 9b6015200f6..68ff183bd44 100644 --- a/Slim/Networking/Slimproto.pm +++ b/Slim/Networking/Slimproto.pm @@ -1,8 +1,7 @@ package Slim::Networking::Slimproto; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Networking/SqueezeNetwork.pm b/Slim/Networking/SqueezeNetwork.pm index f5df9296248..993ad11b26c 100644 --- a/Slim/Networking/SqueezeNetwork.pm +++ b/Slim/Networking/SqueezeNetwork.pm @@ -1,6 +1,9 @@ package Slim::Networking::SqueezeNetwork; -# $Id: SqueezeNetwork.pm 11768 2007-04-16 18:14:55Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. # Async interface to mysqueezebox.com API @@ -10,6 +13,7 @@ use base qw(Slim::Networking::SimpleAsyncHTTP); use Digest::SHA1 qw(sha1_base64); use JSON::XS::VersionOneAndTwo; use MIME::Base64 qw(encode_base64); +use List::Util qw(max); use URI::Escape qw(uri_escape); if ( !main::SCANNER ) { @@ -26,8 +30,6 @@ if ( main::NOMYSB ) { logBacktrace("Support for mysqueezebox.com has been disabled. Please update your code: don't call me if main::NOMYSB."); } -use constant SNTIME_POLL_INTERVAL => 3600; - my $log = logger('network.squeezenetwork'); my $prefs = preferences('server'); @@ -45,11 +47,11 @@ my $nextLoginAttempt = 0; sub get_server { my ($class, $stype) = @_; - + if ( $stype eq 'sn' && $ENV{MYSB_TEST} ) { return $ENV{MYSB_TEST}; } - + return $_Servers->{$stype} || die "No hostname known for server type '$stype'"; } @@ -57,27 +59,23 @@ sub get_server { # Initialize by logging into SN server time and storing our time difference sub init { my $class = shift; - + main::INFOLOG && $log->info('SqueezeNetwork Init'); - + # Convert old non-hashed password if ( my $password = $prefs->get('sn_password') ) { $password = sha1_base64( $password ); $prefs->set( sn_password_sha => $password ); $prefs->remove('sn_password'); - + main::DEBUGLOG && $log->debug('Converted SN password to hashed version'); } - + Slim::Utils::Timers::setTimer( undef, time(), sub { - if ( - ( $prefs->get('sn_email') && $prefs->get('sn_password_sha') ) - || - Slim::Utils::OSDetect::isSqueezeOS() - ) { + if ( $prefs->get('sn_email') && $prefs->get('sn_password_sha') ) { # Login to SN $class->login( cb => \&_init_done, @@ -93,31 +91,29 @@ sub init { sub _init_done { my ( $http, $json ) = @_; - + my $snTime = $json->{time}; - + if ( $snTime !~ /^\d+$/ ) { $http->error( sprintf("Invalid mysqueezebox.com server timestamp (%s)", $http->url) ); return _init_error( $http ); } - + my $diff = $snTime - time(); - + main::INFOLOG && $log->info("Got SqueezeNetwork server time: $snTime, diff: $diff"); - + $prefs->set( sn_timediff => $diff ); - - _syncSNTime_done($http, $snTime); - + # Clear error counter $prefs->remove( 'snInitErrors' ); $loginErrors = $nextLoginAttempt = 0; - + # Store disabled plugins, if any if ( $json->{disabled_plugins} ) { if ( ref $json->{disabled_plugins} eq 'ARRAY' ) { $prefs->set( sn_disabled_plugins => $json->{disabled_plugins} ); - + # Remove disabled plugins from player UI and web UI for my $plugin ( @{ $json->{disabled_plugins} } ) { my $pclass = "Slim::Plugin::${plugin}::Plugin"; @@ -125,24 +121,24 @@ sub _init_done { Slim::Buttons::Home::delSubMenu( $pclass->playerMenu, $pclass->getDisplayName ); main::DEBUGLOG && $log->debug( "Removing $plugin from player UI, service not allowed in country" ); } - + if ( $pclass->can('webPages') && $pclass->can('menu') ) { Slim::Web::Pages->delPageLinks( $pclass->menu, $pclass->getDisplayName ); main::DEBUGLOG && $log->debug( "Removing $plugin from web UI, service not allowed in country" ); } } } - + $prefs->set( sn_disabled_plugins => $json->{disabled_plugins} || [] ); } - + # Init the Internet Radio menu if ( $json->{radio_menu} ) { if ( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::InternetRadio::Plugin') ) { Slim::Plugin::InternetRadio::Plugin->buildMenus( $json->{radio_menu} ); } } - + # Stash the supported protocols if ( $json->{protocolhandlers} && ref $json->{protocolhandlers} eq 'ARRAY') { $prefs->set( sn_protocolhandlers => $json->{protocolhandlers} ); @@ -153,22 +149,22 @@ sub _init_done { require Slim::Networking::SqueezeNetwork::PrefSync; Slim::Networking::SqueezeNetwork::PrefSync->init(); } - + # Init polling for list of SN-connected players Slim::Networking::SqueezeNetwork::Players->init(); - + # Init stats - don't even load the module unless stats are enabled - # let's not bother about re-initialising if pref is changed - there's no user-noticeable effect anyway + # let's not bother about re-initialising if pref is changed - there's no user-noticeable effect anyway # if (!$prefs->get('sn_disable_stats')) { # require Slim::Networking::SqueezeNetwork::Stats; # Slim::Networking::SqueezeNetwork::Stats->init( $json ); # } - + # add link to mysb.com favorites to our local favorites list if ( $json->{favorites_url} ) { my $favs = Slim::Utils::Favorites->new(); - + if ( !defined $favs->findUrl($json->{favorites_url}) ) { $favs->add( $json->{favorites_url}, Slim::Utils::Strings::string('ON_MYSB'), undef, undef, undef, 'html/images/favorites.png' ); @@ -180,25 +176,25 @@ sub _init_done { sub _init_error { my $http = shift; my $error = $http->error; - + $log->error( sprintf("Unable to login to mysqueezebox.com, sync is disabled: $error (%s)", $http->url) ); if ( my $proxy = $prefs->get('webproxy') ) { $log->error( sprintf("Please check your proxy configuration (%s)", $proxy) ); } - + $prefs->remove('sn_timediff'); - + # back off if we keep getting errors my $count = $prefs->get('snInitErrors') || 0; $prefs->set( snInitErrors => $count + 1 ); $loginErrors = $count + 1; - + my $retry = 300 * ( $count + 1 ); $nextLoginAttempt = time() + $retry; - + $log->error( sprintf("mysqueezebox.com sync init failed: $error, will retry in $retry (%s)", $http->url) ); - + Slim::Utils::Timers::setTimer( undef, $nextLoginAttempt + 10, @@ -224,20 +220,20 @@ sub _init_add_non_sn_apps { # Stop all communication with SN, if the user removed their login info for example sub shutdown { my $class = shift; - + $prefs->remove('sn_timediff'); - + # Remove SN session $prefs->remove('sn_session'); - + # Shutdown pref syncing if ( UNIVERSAL::can('Slim::Networking::SqueezeNetwork::PrefSync', 'shutdown') ) { Slim::Networking::SqueezeNetwork::PrefSync->shutdown(); } - + # Shutdown player list fetch Slim::Networking::SqueezeNetwork::Players->shutdown(); - + # Shutdown stats # if ( UNIVERSAL::can('Slim::Networking::SqueezeNetwork::Stats', 'shutdown') ) { # Slim::Networking::SqueezeNetwork::Stats->shutdown(); @@ -251,84 +247,74 @@ sub url { if (main::NOMYSB) { logBacktrace("Support for mysqueezebox.com has been disabled. Please update your code: don't call me if main::NOMYSB."); } - - my $base = 'http://' . $class->get_server('sn'); - + + my $base = (Slim::Networking::Async::HTTP->hasSSL() ? 'https://' : 'http://') . $class->get_server('sn'); + $path ||= ''; - + + $base = '' if $path =~ /^http/; + return $base . $path; } # Is a URL on SN? sub isSNURL { my ( $class, $url ) = @_; - + my $snBase = $class->url(); - + # Allow old SN hostname to be seen as SN my $oldBase = $snBase; $oldBase =~ s/mysqueezebox/squeezenetwork/; - + return $url =~ /^$snBase/ || $url =~ /^$oldBase/; } # Login to SN and obtain a session ID sub login { my ( $class, %params ) = @_; - + $class = ref $class || $class; - + my $client = $params{client}; - + my $time = time(); my $login_params; - + # don't run the query if we've failed recently - if ( $time < $nextLoginAttempt ) { - $log->warn("We've failed to log in a few moments ago. Let's not try again just yet, we don't want to hammer it."); + if ( $time < $nextLoginAttempt && !$params{interactive} ) { + $log->warn("We've failed to log in a few moments ago, or are still waiting for a response. Let's not try again just yet, we don't want to hammer mysqueezebox.com."); return $params{ecb}->(undef, cstring($client, 'SETUP_SN_VALIDATION_FAILED')); } - - if ( Slim::Utils::OSDetect::isSqueezeOS() ) { - # login using MAC/UUID on TinySBS - my $osDetails = Slim::Utils::OSDetect::details(); - - main::INFOLOG && $log->is_info && $log->info("Logging in to " . $_Servers->{sn} . " as " . $osDetails->{mac}); - - $login_params = { - v => 'sc' . $::VERSION, - m => $osDetails->{mac}, - t => $time, - a => sha1_base64( $osDetails->{uuid} . $time ), - }; + + # avoid parallel login attempts + $nextLoginAttempt = max($time + 30, $nextLoginAttempt); + + my $username = $params{username}; + my $password = $params{password}; + + if ( !$username || !$password ) { + $username = $prefs->get('sn_email'); + $password = $prefs->get('sn_password_sha'); } - else { - my $username = $params{username}; - my $password = $params{password}; - - if ( !$username || !$password ) { - $username = $prefs->get('sn_email'); - $password = $prefs->get('sn_password_sha'); - } - - # Return if we don't have any SN login information - if ( !$username || !$password ) { - my $error = cstring($client, 'SQUEEZENETWORK_NO_LOGIN'); - - main::INFOLOG && $log->info( $error ); - return $params{ecb}->( undef, $error ); - } - - main::INFOLOG && $log->is_info && $log->info("Logging in to " . $class->get_server('sn') . " as $username"); - - $login_params = { - v => 'sc' . $::VERSION, - u => $username, - t => $time, - a => sha1_base64( $password . $time ), - }; + + # Return if we don't have any SN login information + if ( !$username || !$password ) { + my $error = cstring($client, 'SQUEEZENETWORK_NO_LOGIN'); + + main::INFOLOG && $log->info( $error ); + return $params{ecb}->( undef, $error ); } - + + main::INFOLOG && $log->is_info && $log->info("Logging in to " . $class->get_server('sn') . " as $username"); + + $login_params = { + v => 'sc' . $::VERSION, + u => $username, + t => $time, + a => sha1_base64( $password . $time ), + }; + my $self = $class->new( \&_login_done, \&_error, @@ -337,127 +323,66 @@ sub login { Timeout => 30, }, ); - + my $url = $self->_construct_url( 'login', $login_params, ); - - if ( Slim::Networking::Async::HTTP->hasSSL() && !delete $params{SSLfailed} ) { - $params{SSL} = 1; - $url =~ s/^http:/https:/; - } $self->get( $url ); } -sub syncSNTime { - # we only want this to run on SqueezeOS/SB Touch - return unless Slim::Utils::OSDetect::isSqueezeOS(); - - my $http = __PACKAGE__->new( - \&_syncSNTime_done, - \&_syncSNTime_done, - ); - - $http->get( $http->url( '/api/v1/time' ) ); -} - -sub _syncSNTime_done { - my ($http, $snTime) = @_; - - # we only want this to run on SqueezeOS/SB Touch - return unless Slim::Utils::OSDetect::isSqueezeOS(); - - if (!$snTime && $http && $http->content) { - $snTime = $http->content; - } - - if ( $snTime && $snTime =~ /^\d+$/ && $snTime > 1262887372 ) { - main::INFOLOG && $log->info("Got SqueezeNetwork server time - set local time to $snTime"); - - # update offset to SN time - $prefs->set( sn_timediff => $snTime - time() ); - - # set local time to mysqueezebox.com's epochtime - Slim::Control::Request::executeRequest(undef, ['date', "set:$snTime"]); - } - else { - $log->error("Invalid or no mysqueezebox.com server timestamp - ignoring"); - } - - Slim::Utils::Timers::killTimers( undef, \&syncSNTime ); - Slim::Utils::Timers::setTimer( - undef, - time() + SNTIME_POLL_INTERVAL, - \&syncSNTime, - ); - -} - - sub getHeaders { my ( $self, $client ) = @_; - + my @headers; - + # Add player ID data if ( $client ) { push @headers, 'X-Player-MAC', $client->master()->id; if ( my $uuid = $client->master()->uuid ) { push @headers, 'X-Player-UUID', $uuid; } - + # Add device id/firmware info if ( $client->deviceid ) { push @headers, 'X-Player-DeviceInfo', $client->deviceid . ':' . $client->revision; } - + # Add player name my $name = $client->name; utf8::encode($name); push @headers, 'X-Player-Name', encode_base64( $name, '' ); - + push @headers, 'X-Player-Model', $client->model; - + # Bug 13963, Add "controlled by" string so SN knows what kind of menu to return if ( my $controller = $client->controlledBy ) { push @headers, 'X-Controlled-By', $controller; } - + if ( my $controllerUA = $client->controllerUA ) { push @headers, 'X-Controller-UA', $controllerUA; } - + # Add Accept-Language header my $lang = $client->languageOverride(); # override from comet request - + $lang ||= $prefs->get('language') || 'en'; - + push @headers, 'Accept-Language', lc($lang); - + # Request JSON instead of XML, it is much faster to parse push @headers, 'Accept', 'text/x-json, text/xml'; } - + return @headers; } sub getAuthHeaders { my ( $self ) = @_; - - if ( Slim::Utils::OSDetect::isSqueezeOS() ) { - - # login using MAC/UUID on TinySBS - my $osDetails = Slim::Utils::OSDetect::details(); - my $time = time(); - - return [ - sn_auth_u => $osDetails->{mac} . '|' . $time . '|' . sha1_base64( $osDetails->{uuid} . $time ), - ]; - } - + my $email = $prefs->get('sn_email') || ''; my $pass = $prefs->get('sn_password_sha') || ''; @@ -468,40 +393,40 @@ sub getAuthHeaders { sub getCookie { my ( $self, $client ) = @_; - + # Add session cookie if we have it if ( my $sid = $prefs->get('sn_session') ) { return 'sdi_squeezenetwork_session=' . uri_escape($sid); } - + return; } # Override to add session cookie header sub _createHTTPRequest { my ( $self, $type, $url, @args ) = @_; - + # Add SN-specific headers unshift @args, $self->getHeaders( $self->params('client') ); - + my $cookie; if ( $cookie = $self->getCookie( $self->params('client') ) ) { unshift @args, 'Cookie', $cookie; } - + if ( !$cookie && $url !~ m{api/v1/(login|radio)|public|update} ) { main::INFOLOG && $log->info("Logging in to SqueezeNetwork to obtain session ID"); - + # Login and get a session ID $self->login( client => $self->params('client'), cb => sub { if ( my $cookie = $self->getCookie( $self->params('client') ) ) { unshift @args, 'Cookie', $cookie; - + main::INFOLOG && $log->info('Got SqueezeNetwork session ID'); } - + $self->SUPER::_createHTTPRequest( $type, $url, @args ); }, ecb => sub { @@ -510,47 +435,61 @@ sub _createHTTPRequest { $self->ecb->( $self, $error ); }, ); - + return; } - + +=pod + # when dealing with an https url, wrap the error handler in some code to fall back to http on failure + if ($url =~ /^https:/) { + my $ecb = $self->ecb; + + $self->ecb(sub { + my ($self, $error) = @_; + + # XXX - fallback should probably only be used if we failed du to some https issue + # Connect timed out: Connection refused - https not available on server + $log->error("Failed to fetch $url: $error"); + $url =~ s/^https:/http:/; + $log->warn("https lookup failed - trying plain text http instead: $url"); + + $self->ecb($ecb); + $self->SUPER::_createHTTPRequest( $type, $url, @args); + }); + } +=cut + $self->SUPER::_createHTTPRequest( $type, $url, @args ); } sub _login_done { my $self = shift; my $params = $self->params('params'); - + my $json = eval { from_json( $self->content ) }; - + if ( $@ ) { return $self->_error( $@ ); } - + if ( $json->{error} ) { return $self->_error( $json->{error} ); } - + if ( my $sid = $json->{sid} ) { $prefs->set( sn_session => $sid ); } - + $nextLoginAttempt = $loginErrors = 0; - + main::DEBUGLOG && $log->debug("Logged into SN OK"); - + $params->{cb}->( $self, $json ); } sub _error { my ( $self, $error ) = @_; my $params = $self->params('params'); - - if ( delete $params->{SSL} ) { - $params->{SSLfailed} = 1; - $self->login(%$params); - return; - } # tell the login method not to try again $loginErrors++; @@ -561,19 +500,19 @@ sub _error { $log->error( "Unable to login to SN: $error" . ($proxy ? sprintf(" - please check your proxy configuration (%s)", $proxy) : '') ); - + $prefs->remove('sn_session'); - + $self->error( $error ); - + $params->{ecb}->( $self, $error ); } sub _construct_url { my ( $self, $method, $params ) = @_; - + my $url = $self->url( '/api/v1/' . $method ); - + if ( my @keys = keys %{$params} ) { my @params; foreach my $key ( @keys ) { @@ -581,11 +520,11 @@ sub _construct_url { } $url .= '?' . join( '&', @params ); } - + return $url; } 1; - - - + + + diff --git a/Slim/Networking/SqueezeNetwork/Players.pm b/Slim/Networking/SqueezeNetwork/Players.pm index 889a95ed200..dfbf9434f14 100644 --- a/Slim/Networking/SqueezeNetwork/Players.pm +++ b/Slim/Networking/SqueezeNetwork/Players.pm @@ -1,6 +1,5 @@ package Slim::Networking::SqueezeNetwork::Players; -# $Id$ # Keep track of players that are connected to SN @@ -28,16 +27,17 @@ my $INACTIVE_PLAYERS = []; # Default polling time use constant MIN_POLL_INTERVAL => 60; my $POLL_INTERVAL = MIN_POLL_INTERVAL; +my $fetching; sub init { my $class = shift; - + # CLI command for telling a player on SN to connect to us Slim::Control::Request::addDispatch( ['squeezenetwork', 'disconnect', '_id'], [0, 1, 0, \&disconnect_player] ); - + # CLI command to trigger a player fetch Slim::Control::Request::addDispatch( ['squeezenetwork', 'fetch_players', '_id'], @@ -52,7 +52,7 @@ sub init { undef, time() + 2, \&fetch_players, - ); + ); }, [['client'],['new','reconnect','disconnect','forget']] ); @@ -61,17 +61,17 @@ sub init { undef, time() + 3, \&fetch_players, - ); + ); } sub shutdown { my $class = shift; - + $CONNECTED_PLAYERS = []; $INACTIVE_PLAYERS = []; - + Slim::Utils::Timers::killTimers( undef, \&fetch_players ); - + main::INFOLOG && $log->info( "SqueezeNetwork player list shutdown" ); } @@ -80,48 +80,88 @@ sub fetch_players { if (main::NOMYSB) { } else { # XXX: may want to improve this for client new/disconnect/reconnect/forget to only fetch # player into for that single player - + + # don't run this call if we're already waiting for player information + if ($fetching++) { + $log->warn("Ignoring request to get player information from mysqueezebox.com. A request is already running ($fetching)"); + return; + } + Slim::Utils::Timers::killTimers( undef, \&fetch_players ); - + # Get the list of players for our account that are on SN my $http = Slim::Networking::SqueezeNetwork->new( \&_players_done, \&_players_error, ); - + $http->get( $http->url( '/api/v1/players' ) ); } } sub _players_done { my $http = shift; - + my $res = eval { from_json( $http->content ) }; if ( $@ || ref $res ne 'HASH' || $res->{error} ) { $http->error( $@ || 'Invalid JSON response: ' . $http->content ); return _players_error( $http ); } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Got list of SN players: " . Data::Dump::dump( $res->{players}, $res->{inactive_players} ) ); - $log->debug( "Got list of active services: " . Data::Dump::dump( $res->{active_services} )); - $log->debug( "Next player check in " . $res->{next_poll} . " seconds" ); + $log->debug( "Got list of all apps : " . Data::Dump::dump( $res->{all_apps} )); + $log->debug( "Next player check in : " . $res->{next_poll} . " seconds" ); } - + # Update poll interval with advice from SN $POLL_INTERVAL = $res->{next_poll}; - + # Make sure poll interval isn't too small if ( $POLL_INTERVAL < MIN_POLL_INTERVAL ) { $POLL_INTERVAL = MIN_POLL_INTERVAL; } - + # Update player list $CONNECTED_PLAYERS = $res->{players}; $INACTIVE_PLAYERS = $res->{inactive_players}; - + + # SN can provide string translations for new menu items + if ( $res->{strings} ) { + main::DEBUGLOG && $log->is_debug && $log->debug( 'Adding SN-supplied strings: ' . Data::Dump::dump( $res->{strings} ) ); + + Slim::Utils::Strings::storeExtraStrings( $res->{strings} ); + } + + _registerApps($res); + + # SN can provide string translations for new menu items + if ( $res->{search_providers} ) { + main::DEBUGLOG && $log->is_debug && $log->debug( 'Adding search providers: ' . Data::Dump::dump( $res->{search_providers} ) ); + + Slim::Menu::GlobalSearch->registerSearchProviders( $res->{search_providers} ); + } + + + # Clear error count if any + if ( $prefs->get('snPlayersErrors') ) { + $prefs->remove('snPlayersErrors'); + } + + $fetching = 0; + + Slim::Utils::Timers::setTimer( + undef, + time() + $POLL_INTERVAL, + \&fetch_players, + ); +} + +sub _registerApps { + my ($res) = @_; + # Make a list of all apps for the web UI - my $allApps = {}; - + my $allApps = $res->{all_apps} || {}; + # Add 3rd party plugins which have requested to be on the apps menu if (my $nonSNApps = Slim::Plugin::Base->nonSNApps) { for my $plugin (@$nonSNApps) { @@ -131,79 +171,47 @@ sub _players_done { } } - # SN can provide string translations for new menu items - if ( $res->{strings} ) { - main::DEBUGLOG && $log->is_debug && $log->debug( 'Adding SN-supplied strings: ' . Data::Dump::dump( $res->{strings} ) ); - - Slim::Utils::Strings::storeExtraStrings( $res->{strings} ); - } - - # Update enabled apps for each player - my $appHandler = sub { - my ($player, $cprefs, $client) = @_; - - # Compare existing apps to new list - my $currentApps = complex_to_query( $cprefs->get('apps') || {} ); - my $newApps = complex_to_query( $player->{apps} ); - - # Only refresh menus if the list has changed - if ( $currentApps ne $newApps ) { - $cprefs->set( apps => $player->{apps} ); - - $client ||= Slim::Player::Client::getClient( $player->{mac} ); - - # Refresh ip3k and Jive menu - if ( $client ) { - if ( !$client->isa('Slim::Player::SqueezePlay') ) { - Slim::Buttons::Home::updateMenu($client); - } - - # Clear Jive menu and refresh with new main menu - Slim::Control::Jive::deleteAllMenuItems($client); - Slim::Control::Jive::mainMenu($client); - } - } - }; - - # This will create new pref entries for players this server has never seen my %playersSeen; for my $player ( @{ $res->{players} }, @{ $res->{inactive_players} } ) { if ( exists $player->{apps} ) { $playersSeen{$player->{mac}}++; - + # Keep a list of all available apps for the web UI for my $app ( keys %{ $player->{apps} } ) { $allApps->{$app} = $player->{apps}->{$app}; } - + + # Don't create new pref entries for players this server has never seen + next unless Slim::Utils::Prefs::Client->hasPrefs($prefs, $player->{mac}); + my $cprefs = Slim::Utils::Prefs::Client->new( $prefs, $player->{mac}, 'no-migrate' ); - - $appHandler->($player, $cprefs); + + _appHandler($player, $cprefs); } } # now do the same for all locally connected players which are not known by mysb.com (eg. software players) for my $client ( Slim::Player::Client::clients() ) { next if !$client->macaddress || $playersSeen{$client->macaddress}; - - $appHandler->( { + + _appHandler( { mac => $client->macaddress, apps => $allApps, }, $prefs->client($client), $client ); } - + # Setup apps for the web and classic player UI. if ( main::WEBUI ) { # Clear all existing my_apps items on the web, we'll build a new list Slim::Web::Pages->delPageCategory('my_apps'); - + for my $app ( keys %{$allApps} ) { - + # don't initialize if we have a local plugin overriding the mysb.com service next if $allApps->{'nonsn_' . $app}; - + my $info = $allApps->{$app}; - + # If this app is supported by a local plugin, we'll use the webpage already setup for it # and just copy it to the my_apps list if ( $info->{plugin} ) { @@ -218,12 +226,12 @@ sub _players_done { if ( $icon !~ /^http/ ) { $icon = Slim::Networking::SqueezeNetwork->url($icon); } - + my $feed = $info->{url}; if ( $feed !~ /^http/ ) { $feed = Slim::Networking::SqueezeNetwork->url($feed); } - + my $tag = lc($app); # dynamically create plugin code for mysb.com based apps @@ -235,20 +243,20 @@ sub _players_done { if (!$@) { main::DEBUGLOG && $log->is_debug && $log->debug("Plugin $subclass already initialized - skipping"); _updateWebLink($info->{title}, $app); - next; + next; } main::DEBUGLOG && $log->is_debug && $log->debug("Initializing plugin for mysqueezebox.com based app '$app': $subclass"); - + my $code = qq{ package ${subclass}; - + use strict; use base qw(Slim::Plugin::OPMLBased); - + sub initPlugin { my \$class = shift; - + \$class->SUPER::initPlugin( tag => '$tag', menu => 'apps', @@ -257,24 +265,24 @@ sub _players_done { is_app => 1, ); } - + sub _pluginDataFor { my ( \$class, \$key ) = \@_; - + return '$icon' if \$key eq 'icon'; - + return \$class->SUPER::_pluginDataFor(\$key); } sub getDisplayName { '$info->{title}' } - + sub playerMenu { } - - 1; + + 1; }; - + eval $code; - + if ($@) { $log->error( "Unable to dynamically create plugin class $subclass: $@" ); } @@ -292,45 +300,55 @@ sub _players_done { } } } +} - # SN can provide string translations for new menu items - if ( $res->{search_providers} ) { - main::DEBUGLOG && $log->is_debug && $log->debug( 'Adding search providers: ' . Data::Dump::dump( $res->{search_providers} ) ); +# Update enabled apps for each player +sub _appHandler { + my ($player, $cprefs, $client) = @_; - Slim::Menu::GlobalSearch->registerSearchProviders( $res->{search_providers} ); - } - - - # Clear error count if any - if ( $prefs->get('snPlayersErrors') ) { - $prefs->remove('snPlayersErrors'); + return unless $cprefs->get('playername'); + + # Compare existing apps to new list + my $currentApps = complex_to_query( $cprefs->get('apps') || {} ); + my $newApps = complex_to_query( $player->{apps} ); + + # Only refresh menus if the list has changed + if ( $currentApps ne $newApps ) { + $cprefs->set( apps => $player->{apps} ); + + $client ||= Slim::Player::Client::getClient( $player->{mac} ); + + # Refresh ip3k and Jive menu + if ( $client ) { + if ( !$client->isa('Slim::Player::SqueezePlay') ) { + Slim::Buttons::Home::updateMenu($client); + } + + # Clear Jive menu and refresh with new main menu + Slim::Control::Jive::deleteAllMenuItems($client); + Slim::Control::Jive::mainMenu($client); + } } - - Slim::Utils::Timers::setTimer( - undef, - time() + $POLL_INTERVAL, - \&fetch_players, - ); } sub _updateWebLink { my $name = shift; my $id = shift; my $info = shift; - + my $disabled = $prefs->get('sn_disabled_plugins'); return if $disabled && grep /^$id$/i, @$disabled; - + if ($info && $info->{title} && $name && $info->{title} ne $name) { my $url = Slim::Web::Pages->getPageLink( 'apps', $name ); Slim::Web::Pages->addPageLinks( 'my_apps', { $info->{title} => $url } ); - + # use icon as defined by MySB to allow for white-label solutions if ( my $icon = $info->{icon} ) { my $pluginData = Slim::Utils::PluginManager->dataForPlugin($info->{plugin}); $icon = Slim::Networking::SqueezeNetwork->url( $icon, 'external' ) unless $icon =~ /^http/; $pluginData->{icon} = $icon; - + Slim::Web::Pages->addPageLinks("icons", { $name => $icon }); Slim::Web::Pages->addPageLinks("icons", { $info->{title} => $icon }); } @@ -344,20 +362,25 @@ sub _updateWebLink { sub _players_error { my $http = shift; my $error = $http->error; - + $prefs->remove('sn_session'); - + # We don't want a stale list of players, so clear it out on error $CONNECTED_PLAYERS = []; $INACTIVE_PLAYERS = []; - + # Backoff if we keep getting errors my $count = $prefs->get('snPlayersErrors') || 0; $prefs->set( snPlayersErrors => $count + 1 ); my $retry = $POLL_INTERVAL * ( $count + 1 ); - + $log->error( "Unable to get players from SN: $error, retrying in $retry seconds" ); - + + # let's still register apps, as some are local only anyway + _registerApps({}); + + $fetching = 0; + Slim::Utils::Timers::setTimer( undef, time() + $retry, @@ -369,7 +392,7 @@ sub get_players { if (main::NOMYSB) { logBacktrace("Support for mysqueezebox.com has been disabled. Please update your code: don't call me if main::NOMYSB."); } else { my $class = shift; - + return wantarray ? @{$CONNECTED_PLAYERS} : $CONNECTED_PLAYERS; } } @@ -377,7 +400,7 @@ sub is_known_player { if (main::NOMYSB) { logBacktrace("Support for mysqueezebox.com has been disabled. Please update your code: don't call me if main::NOMYSB."); } else { my ($class, $client) = @_; - + my $mac = ref($client) ? $client->macaddress() : $client; return scalar( grep { $mac eq $_->{mac} } @{$CONNECTED_PLAYERS}, @{$INACTIVE_PLAYERS} ); @@ -386,9 +409,9 @@ sub is_known_player { if (main::NOMYSB) { sub disconnect_player { my $request = shift; my $id = $request->getParam('_id') || return; - + $request->setStatusProcessing(); - + # Tell an SN player to reconnect to our IP my $http = Slim::Networking::SqueezeNetwork->new( \&_disconnect_player_done, @@ -397,31 +420,31 @@ sub disconnect_player { request => $request, } ); - + my $ip = Slim::Utils::Network::serverAddr(); - + $http->get( $http->url( '/api/v1/players/disconnect/' . $id . '/' . $ip ) ); } sub _disconnect_player_done { my $http = shift; my $request = $http->params('request'); - + my $res = eval { from_json( $http->content ) }; if ( $@ || ref $res ne 'HASH' ) { $http->error( $@ || 'Invalid JSON response' ); return _disconnect_player_error( $http ); } - + if ( $res->{error} ) { $http->error( $res->{error} ); return _disconnect_player_error( $http ); } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Disconect SN player response: " . Data::Dump::dump( $res ) ); } - + $request->setStatusDone(); } @@ -429,12 +452,12 @@ sub _disconnect_player_error { my $http = shift; my $error = $http->error; my $request = $http->params('request'); - + $log->error( "Disconnect SN player error: $error" ); - + $request->addResult( error => $error ); - + $request->setStatusDone(); -} +} 1; diff --git a/Slim/Networking/SqueezeNetwork/PrefSync.pm b/Slim/Networking/SqueezeNetwork/PrefSync.pm index 7a6ee04ec7e..d881a2a6f09 100644 --- a/Slim/Networking/SqueezeNetwork/PrefSync.pm +++ b/Slim/Networking/SqueezeNetwork/PrefSync.pm @@ -1,6 +1,9 @@ package Slim::Networking::SqueezeNetwork::PrefSync; -# $Id: SqueezeNetwork.pm 11768 2007-04-16 18:14:55Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. # Sync prefs from SS <-> SN @@ -58,7 +61,7 @@ sub _init_done { # Sync all connected clients with SN # New clients will be handled by clientEvent for my $client ( Slim::Player::Client::clients() ) { - next unless $client->isa('Slim::Player::Squeezebox2') && $client->deviceid !~ /^(?:3|6|8|11|12)/; + next unless $client->isa('Slim::Player::Squeezebox2') && $client->deviceid < 11 && $client->deviceid !~ /^(?:3|6|8|11|12)/; Slim::Utils::Timers::setTimer( $client, diff --git a/Slim/Networking/SqueezeNetwork/Stats.pm b/Slim/Networking/SqueezeNetwork/Stats.pm index 9445e589ca3..ba6632f02e3 100644 --- a/Slim/Networking/SqueezeNetwork/Stats.pm +++ b/Slim/Networking/SqueezeNetwork/Stats.pm @@ -1,6 +1,5 @@ package Slim::Networking::SqueezeNetwork::Stats; -# $Id$ # Report radio stats to SN if enabled. @@ -19,7 +18,7 @@ my $log = logger('network.squeezenetwork'); my $prefs = preferences('server'); # Regex for which URLs we want to report stats for -my $REPORT_RE = qr{^(?:http|mms|live365|loop)://}; +my $REPORT_RE = qr{^(?:http|mms|loop)://}; # Report stats to SN at this interval my $REPORT_INTERVAL = 1200; diff --git a/Slim/Networking/UDP.pm b/Slim/Networking/UDP.pm index 238d45eaa5f..7e83302f877 100644 --- a/Slim/Networking/UDP.pm +++ b/Slim/Networking/UDP.pm @@ -1,8 +1,7 @@ package Slim::Networking::UDP; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Boom.pm b/Slim/Player/Boom.pm index d4c3e9209c9..4574adfff5c 100644 --- a/Slim/Player/Boom.pm +++ b/Slim/Player/Boom.pm @@ -1,6 +1,6 @@ package Slim::Player::Boom; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/CapabilitiesHelper.pm b/Slim/Player/CapabilitiesHelper.pm index c825ba4f48a..2d78158970b 100644 --- a/Slim/Player/CapabilitiesHelper.pm +++ b/Slim/Player/CapabilitiesHelper.pm @@ -1,8 +1,7 @@ package Slim::Player::CapabilitiesHelper; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Client.pm b/Slim/Player/Client.pm index b4ef75d9e31..5e055b30da8 100644 --- a/Slim/Player/Client.pm +++ b/Slim/Player/Client.pm @@ -1,8 +1,7 @@ package Slim::Player::Client; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -126,7 +125,7 @@ use constant KNOB_NOACCELERATION => 0x02; curDepth lastLetterIndex lastLetterDigit lastLetterTime lastDigitIndex lastDigitTime searchFor syncSelection _playPoint playPoints jiffiesEpoch jiffiesOffsetList - _tempVolume musicInfoTextCache metaTitle languageOverride controlledBy controllerUA password currentSleepTime + _tempVolume browseCache musicInfoTextCache metaTitle languageOverride controlledBy controllerUA password currentSleepTime sleepTime pendingPrefChanges _pluginData alarmData knobData modeStack modeParameterStack playlist chunks @@ -137,7 +136,7 @@ use constant KNOB_NOACCELERATION => 0x02; __PACKAGE__->mk_accessor('hash', qw( curSelection lastID3Selection )); - + # modeParameterStack is called a lot, cache the index to avoid many accessor calls $modeParameterStackIndex = __PACKAGE__->_slot('modeParameterStack'); } @@ -167,7 +166,7 @@ sub new { if ( defined $uuid && $uuid =~ /0000000000/ ) { $uuid = undef; } - + $client->init_accessor( # device identify @@ -209,7 +208,7 @@ sub new { #It is used to allow the player to act as the master for the locally maintained parameter. sequenceNumber => 0, - # The (controllerSequenceId, controllerSequenceNumber) tuple is used to enable synchronization of commands + # The (controllerSequenceId, controllerSequenceNumber) tuple is used to enable synchronization of commands # sent to the player via the server and via an additional, out-of-band mechanism (currently UDAP). # It is used to enable the player to discard duplicate commands received via both channels. controllerSequenceId => undef, @@ -251,7 +250,7 @@ sub new { currentPlaylistRender => undef, _currentPlaylistUpdateTime => Time::HiRes::time(), # only changes to the playlist _currentPlaylistChangeTime => undef, # updated on song changes - + # display state display => undef, lines => undef, @@ -283,15 +282,16 @@ sub new { playPoints => undef, # set of (timeStamp, apparentStartTime) tuples to determine consistency jiffiesEpoch => undef, jiffiesOffsetList => [], # array tracking the relative deviations relative to our clock - + # alarm state alarmData => {}, # Stored alarm data for this client. Private. - + # Knob data knobData => {}, # Stored knob data for this client # other _tempVolume => undef, + browseCache => {}, musicInfoTextCache => undef, metaTitle => undef, languageOverride => undef, @@ -304,14 +304,14 @@ sub new { _pluginData => {}, updatePending => 0, disconnected => 0, - + ); - + $clientHash{$id} = $client; $client->controller(Slim::Player::StreamingController->new($client)); - if (!main::SCANNER) { + if (!main::SCANNER) { Slim::Control::Request::notifyFromArray($client, ['client', 'new']); } @@ -467,7 +467,7 @@ sub name { return $name; } -# If this player does not have a name set, then find the first +# If this player does not have a name set, then find the first # unused name from the sequence ("Name", "Name 2", "Name 3", ...), # where Name, is the result of the modelName() method. Consider # all the players ever known to this SC in finding an unused name. @@ -482,7 +482,7 @@ sub _makeDefaultName { foreach my $clientPref ( $prefs->allClients ) { $existingName{ $clientPref->get('playername') || 'Squeezebox' } = 1; } - + my $maxIndex = 0; do { @@ -512,10 +512,10 @@ sub getClient { # Try a brute for match for the client. if (!defined($ret)) { for my $value ( values %clientHash ) { - return $value if (ipport($value) eq $id); - return $value if (ip($value) eq $id); - return $value if (name($value) eq $id); - return $value if (id($value) eq $id); + return $value if $value->ipport eq $id; + return $value if $value->ip eq $id; + return $value if $value->name eq $id; + return $value if $value->id eq $id; } # none of these matched, so return undef return undef; @@ -533,40 +533,48 @@ the WebUI, nor will any it's timers be serviced anymore until it reconnects. sub forgetClient { my $client = shift; - + if ($client) { $client->controller()->unsync($client, 'keepSyncGroupId'); - + $client->display->forgetDisplay(); - + # Clean up global variables used in various modules Slim::Buttons::Common::forgetClient($client); Slim::Buttons::Home::forgetClient($client); - Slim::Buttons::Input::Choice::forgetClient($client); Slim::Buttons::Playlist::forgetClient($client); Slim::Utils::Alarm->forgetClient($client); Slim::Utils::Timers::forgetTimer($client); - + if ( !main::SCANNER ) { Slim::Web::HTTP::forgetClient($client); } - + delete $clientHash{ $client->id }; - + # stop watching this player delete $Slim::Networking::Slimproto::heartbeat{ $client->id }; - + # Bug 15860: Force the connection shut if it is not already Slim::Networking::Slimproto::slimproto_close($client->tcpsock()) if defined $client->tcpsock(); } } +sub persistPlaybackStateForPowerOff { + my $client = shift; + + if ($client->power()) { + my $playing = $client->controller()->isPlaying(1); + $prefs->client($client)->set('playingAtPowerOff', $playing); + } +} + sub startup { my $client = shift; my $syncgroupid = shift; Slim::Player::Sync::restoreSync($client, $syncgroupid); - + # restore the old playlist Slim::Player::Playlist::loadClientPlaylist($client, \&initial_add_done) } @@ -595,11 +603,11 @@ sub initial_add_done { $i++; } - + $currsong = 0; - + $client->controller()->resetSongqueue($currsong); - + } elsif ($shuffleType eq 'album') { # reshuffle set this properly, for album shuffle @@ -688,6 +696,8 @@ sub canDoReplayGain { return 0; } sub canPowerOff { return 1; } +sub canHTTPS { return 0; } + =head2 mixerConstant( $client, $feature, $aspect ) Returns the requested aspect of a given mixer feature. @@ -944,27 +954,27 @@ Returns a pretty string for the current sleep time. Normally we simply return the time in minutes. -For the case of stopping after the current song, +For the case of stopping after the current song, a friendly string is returned. =cut sub prettySleepTime { my $client = shift; - - + + my $sleeptime = $client->sleepTime() - Time::HiRes::time(); my $sleepstring = ""; - + my $dur = $client->controller()->playingSongDuration() || 0; my $remaining = 0; - + if ($dur) { $remaining = $dur - Slim::Player::Source::songTime($client); } if ($client->sleepTime) { - + # check against remaining time to see if sleep time matches within a minute. if (int($sleeptime/60 + 0.5) == int($remaining/60 + 0.5)) { $sleepstring = $client->string('SLEEPING_AT_END_OF_SONG'); @@ -972,7 +982,7 @@ sub prettySleepTime { $sleepstring = join(" " ,$client->string('SLEEPING_IN'),int($sleeptime/60 + 0.5),$client->string('MINUTES')); } } - + return $sleepstring; } @@ -1024,7 +1034,7 @@ sub modeParam { sub modeParams { my $client = shift; - + @_ ? $client->modeParameterStack()->[-1] = shift : $client->modeParameterStack()->[-1]; } @@ -1104,14 +1114,14 @@ sub streamingProgressBar { my $log = logger('player.streaming'); my $url = $args->{'url'}; - + # Duration specified directly (i.e. from a plugin) my $duration = $args->{'duration'}; - + # Duration can be calculated from bitrate + length my $bitrate = $args->{'bitrate'}; my $length = $args->{'length'}; - + if (main::INFOLOG && $log->is_info) { $log->info(sprintf("url=%s, duration=%s, bitrate=%s, contentLength=%s", $url, @@ -1120,9 +1130,9 @@ sub streamingProgressBar { (defined($length) ? $length : 'undef')) ); } - + my $secs; - + if ( $duration ) { $secs = $duration; } @@ -1132,15 +1142,15 @@ sub streamingProgressBar { else { return; } - + my %cacheEntry = ( 'SECS' => $secs, ); - + Slim::Music::Info::updateCacheEntry( $url, \%cacheEntry ); - + Slim::Music::Info::setDuration( $url, $secs ); - + # Set the duration so the progress bar appears if ( my $song = $client->streamingSong()) { @@ -1173,7 +1183,7 @@ sub epochirtime { $client->lastActivityTime($val); $client->_epochirtime($val); } - + return $client->_epochirtime; } @@ -1225,20 +1235,20 @@ sub currentPlaylistChangeTime { sub pluginData { my ( $client, $key, $value ) = @_; - + my $namespace; - + # if called from a plugin, we automatically use the plugin's namespace for keys my $package = caller(0); - + if ( $package =~ /^(?:Slim::Plugin|Plugins)::(\w+)/ ) { $namespace = $1; } - + if ( $namespace && !defined $key ) { return $client->_pluginData->{$namespace}; } - + if ( defined $value ) { if ( $namespace ) { $client->_pluginData->{$namespace}->{$key} = $value; @@ -1247,7 +1257,7 @@ sub pluginData { $client->_pluginData->{$key} = $value; } } - + if ( $namespace ) { my $val = $client->_pluginData->{$namespace}->{$key}; return ( defined $val ) ? $val : undef; @@ -1285,7 +1295,7 @@ Set a preset for this player. Arguments: =over 4 -=item slot +=item slot Which preset to set. Valid values are from 1-10. @@ -1311,17 +1321,17 @@ Optional. XMLBrowser parser. sub setPreset { my ( $client, $args ) = @_; - + return unless $args->{slot} && $args->{URL} && $args->{text}; - + my $preset = { URL => $args->{URL}, text => $args->{text}, type => $args->{type} || 'audio', }; - $preset->{parser} = $args->{parser} if $args->{parser}; - + + my $cprefs = $prefs->client($client); my $presets = $cprefs->get('presets'); $presets->[ $args->{slot} - 1 ] = $preset; @@ -1336,7 +1346,7 @@ sub master {return $_[0]->controller()->master();} sub streamingSong {return $_[0]->controller()->streamingSong();} sub playingSong {return $_[0]->controller()->playingSong();} - + sub isPlaying {return $_[0]->controller()->isPlaying($_[1]);} sub isPaused {return $_[0]->controller()->isPaused();} sub isStopped {return $_[0]->controller()->isStopped();} @@ -1344,7 +1354,7 @@ sub isRetrying {return $_[0]->controller()->isRetrying();} sub currentTrackForUrl { my ($client, $url) = @_; - + my $song = $client->controller()->currentSongForUrl($url); if ( $song ) { return $song->currentTrack(); @@ -1353,7 +1363,7 @@ sub currentTrackForUrl { sub currentSongForUrl { my ($client, $url) = @_; - + return $client->controller()->currentSongForUrl($url); } @@ -1397,7 +1407,7 @@ sub syncedWithNames { return join(' & ', map { $_->name || $_->id } @syncList); } - + # return formatted date/time strings - overwritten in SN to respect timezone sub timeF { return Slim::Utils::DateTime::timeF( @@ -1433,7 +1443,7 @@ sub hasScrolling { 0 } sub apps { my $client = shift; - + my %clientApps = %{$prefs->client($client)->get('apps') || {}}; if (my $nonSNApps = Slim::Plugin::Base->nonSNApps) { @@ -1449,11 +1459,11 @@ sub apps { sub isAppEnabled { my ( $client, $app ) = @_; - + if ( grep { $_ eq lc($app) } keys %{ $client->apps } ) { return 1; } - + return; } diff --git a/Slim/Player/Disconnected.pm b/Slim/Player/Disconnected.pm index b7d0c93f21c..b115bdfc8fa 100644 --- a/Slim/Player/Disconnected.pm +++ b/Slim/Player/Disconnected.pm @@ -1,4 +1,4 @@ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/HTTP.pm b/Slim/Player/HTTP.pm index c63b214b133..dc505288c48 100644 --- a/Slim/Player/HTTP.pm +++ b/Slim/Player/HTTP.pm @@ -1,4 +1,4 @@ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Pipeline.pm b/Slim/Player/Pipeline.pm index b880c8786ae..feb116943bb 100644 --- a/Slim/Player/Pipeline.pm +++ b/Slim/Player/Pipeline.pm @@ -1,8 +1,7 @@ package Slim::Player::Pipeline; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -47,7 +46,7 @@ sub new { }; my $readerPort = $listenReader->sockport; - + my ($listenWriter, $writerPort); if ($source) { @@ -69,10 +68,10 @@ sub new { $command =~ s/"/\\"/g; my $newcommand = '"' . Slim::Utils::Misc::findbin('socketwrapper') . '" '; - + # Bug 15650, Run transcoders with the same priority as the server # XXX this sets the priority of the socketwrapper.exe process but not the actual transcoder process(es). - my $priority = Slim::Utils::OS::Win32::getPriorityClass() || Win32::Process::NORMAL_PRIORITY_CLASS(); + my $priority = Win32::Process::NORMAL_PRIORITY_CLASS(); my $createMode = $priority | Win32::Process::CREATE_NO_WINDOW(); @@ -146,7 +145,7 @@ sub new { return undef; } - + if (!defined(Slim::Utils::Network::blocking($writer, 0))) { logError("Cannot set pipe line writer to nonblocking"); @@ -156,7 +155,7 @@ sub new { return undef; } - + binmode($reader); binmode($writer); } @@ -322,16 +321,16 @@ sub sysread { ${*$self}{'pipeline_pending_bytes'} = $pendingBytes; ${*$self}{'pipeline_pending_size'} = $pendingSize; - + if ($! != EWOULDBLOCK) { return undef; # reflect error to caller } - + last STUFF_PIPE; } } - + return $reader->sysread($_[1], $chunksize); } diff --git a/Slim/Player/Player.pm b/Slim/Player/Player.pm index b1272004703..6b3b2e1e8c8 100644 --- a/Slim/Player/Player.pm +++ b/Slim/Player/Player.pm @@ -1,6 +1,6 @@ package Slim::Player::Player; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -10,7 +10,6 @@ package Slim::Player::Player; # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # -# $Id$ # use strict; @@ -70,6 +69,7 @@ our $defaultPrefs = { 'packetLatency' => 2, # ms 'startDelay' => 0, # ms 'playDelay' => 0, # ms + 'fadeInDuration' => 0, }; $prefs->setChange( sub { $_[2]->volume($_[1]); }, 'volume'); @@ -109,8 +109,8 @@ sub init { Slim::Buttons::Home::updateMenu($client); # fire it up! - $client->power($prefs->client($client)->get('power')); $client->startup($syncgroupid); + $client->power($prefs->client($client)->get('power'), 0, 1); return if $client->display->isa('Slim::Display::NoDisplay'); @@ -201,11 +201,12 @@ sub power { my $client = shift; my $on = shift; my $noplay = shift; + my $force = shift; my $currOn = $prefs->client($client)->get('power') || 0; return $currOn unless defined $on; - return unless (!defined(Slim::Buttons::Common::mode($client)) || ($currOn != $on)); + return unless (!defined(Slim::Buttons::Common::mode($client)) || ($currOn != $on)) || $force; my $resume = $prefs->client($client)->get('powerOnResume'); $resume =~ /(.*)Off-(.*)On/; @@ -357,7 +358,10 @@ sub fade_volume { my $int = 0.05; # interval between volume updates - my $vol = abs($prefs->client($client)->get("volume")); + # start from current position if still ramping up + my $vol = $fade < 0 && abs($client->volume) < abs($prefs->client($client)->get("volume")) ? + abs($client->volume) : abs($prefs->client($client)->get("volume")); + my $now = Time::HiRes::time(); Slim::Utils::Timers::killHighTimers($client, \&_fadeVolumeUpdate); diff --git a/Slim/Player/Playlist.pm b/Slim/Player/Playlist.pm index fa236b050cb..2711b0e7501 100644 --- a/Slim/Player/Playlist.pm +++ b/Slim/Player/Playlist.pm @@ -1,6 +1,6 @@ package Slim::Player::Playlist; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -626,8 +626,7 @@ sub makeVolatile { # 3. add new tracks # 4. restore position # 5. shuffle again, preserving the currently playing track - my $shuffle = shuffle($client); - $client->execute([ 'playlist', 'shuffle', 0 ]) if $shuffle; + preserveShuffleOrder($client); my $needRestart; @@ -659,36 +658,13 @@ sub makeVolatile { if ($client->isPlaying()) { $playtime = Slim::Player::Source::songTime($client); - if ($shuffle) { - $restoreStateWhenShuffled = ['play', 0.2]; - } - else { - $cmd = 'loadtracks'; - } - } - elsif ($shuffle && $client->power) { - if ($client->isPaused) { - # XXX - pause somehow doesn't work... -# $restoreStateWhenShuffled = ['pause', 1]; - $restoreStateWhenShuffled = ['stop']; - } - elsif ($client->isStopped) { - $restoreStateWhenShuffled = ['stop']; - } + $cmd = 'loadtracks'; } - + Slim::Player::Playlist::stopAndClear($client); $client->execute([ 'playlist', $cmd, 'listRef', \@urls, 0.2, $position ]); - # restore shuffle state - if ($shuffle) { - # playlist addtracks wouldn't jump - need to do it here - $client->execute([ 'playlist', 'jump', $position ]); - $client->execute([ 'playlist', 'shuffle', $shuffle ]); - $client->execute($restoreStateWhenShuffled); - } - Slim::Player::Source::gototime($client, $playtime) if $playtime; } } @@ -914,6 +890,20 @@ sub reshuffle { refreshPlaylist($client); } +# preserveShuffleOrder would order the actual playlist in the currently shuffled order +# After calling this it's no longer possible to restore the original order of tracks. +sub preserveShuffleOrder { + my ($client) = @_; + + if (shuffle($client)) { + my @playlist = @{playList($client)}; + @{$client->playlist} = map { $playlist[$_] } @{shuffleList($client)}; + @{$client->shufflelist} = ( 0 .. $#playlist ); + + shuffle($client, 0); + } +} + sub scheduleWriteOfPlaylist { my ($client, $playlistObj) = @_; diff --git a/Slim/Player/ProtocolHandlers.pm b/Slim/Player/ProtocolHandlers.pm index 2443a5d0dfa..988c71d4379 100644 --- a/Slim/Player/ProtocolHandlers.pm +++ b/Slim/Player/ProtocolHandlers.pm @@ -1,8 +1,7 @@ package Slim::Player::ProtocolHandlers; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -14,6 +13,7 @@ use Scalar::Util qw(blessed); use Slim::Utils::Log; use Slim::Utils::Misc; use Slim::Music::Info; +use Slim::Networking::Async::HTTP; # the protocolHandlers hash contains the modules that handle specific URLs, # indexed by the URL protocol. built-in protocols are exist in the hash, but @@ -22,7 +22,7 @@ my %protocolHandlers = ( file => main::LOCALFILE ? qw(Slim::Player::Protocols::LocalFile) : qw(Slim::Player::Protocols::File), tmp => qw(Slim::Player::Protocols::Volatile), http => qw(Slim::Player::Protocols::HTTP), - https => qw(Slim::Player::Protocols::HTTP), + https => Slim::Networking::Async::HTTP->hasSSL() ? qw(Slim::Player::Protocols::HTTPS) : qw(Slim::Player::Protocols::HTTP), icy => qw(Slim::Player::Protocols::HTTP), mms => qw(Slim::Player::Protocols::MMS), spdr => qw(Slim::Player::Protocols::SqueezePlayDirect), @@ -126,6 +126,8 @@ sub iconHandlerForURL { sub iconForURL { my ($class, $url, $client) = @_; + + $url ||= ''; if (my $handler = $class->handlerForURL($url)) { if ($client && $handler->can('getMetadataFor')) { diff --git a/Slim/Player/Protocols/File.pm b/Slim/Player/Protocols/File.pm index 22538bdefec..912dbc91deb 100644 --- a/Slim/Player/Protocols/File.pm +++ b/Slim/Player/Protocols/File.pm @@ -1,8 +1,7 @@ package Slim::Player::Protocols::File; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech, Vidur Apparao. +# Logitech Media Server Copyright 2001-2020 Logitech, Vidur Apparao. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Protocols/HTTP.pm b/Slim/Player/Protocols/HTTP.pm index 0c1e506643e..d5209a145b4 100644 --- a/Slim/Player/Protocols/HTTP.pm +++ b/Slim/Player/Protocols/HTTP.pm @@ -1,11 +1,10 @@ package Slim::Player::Protocols::HTTP; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech, Vidur Apparao. +# Logitech Media Server Copyright 2001-2020 Logitech, Vidur Apparao. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, -# version 2. +# version 2. use strict; use base qw(Slim::Formats::RemoteStream); @@ -38,7 +37,7 @@ sub new { if (!$args->{'song'}) { logWarning("No song passed!"); - + # XXX: MusicIP abuses this as a non-async HTTP client, can't return undef # return undef; } @@ -75,21 +74,21 @@ sub readMetaData { } else { - #$log->debug("Metadata byte not read, trying again: $!"); + #$log->debug("Metadata byte not read, trying again: $!"); } } $byteRead = defined $byteRead ? $byteRead : 0; } - + $metadataSize = ord($metadataSize) * 16; - + if ($metadataSize > 0) { main::DEBUGLOG && $log->debug("Metadata size: $metadataSize"); - + my $metadata; my $metadatapart; - + do { $metadatapart = ''; $byteRead = $self->SUPER::sysread($metadatapart, $metadataSize); @@ -107,10 +106,10 @@ sub readMetaData { } $byteRead = 0 if (!defined($byteRead)); - $metadataSize -= $byteRead; - $metadata .= $metadatapart; + $metadataSize -= $byteRead; + $metadata .= $metadatapart; - } while ($metadataSize > 0); + } while ($metadataSize > 0); main::INFOLOG && $log->info("Metadata: $metadata"); @@ -131,14 +130,14 @@ sub parseMetadata { my $url = Slim::Player::Playlist::url( $client, Slim::Player::Source::streamingSongIndex($client) ); - + # See if there is a parser for this stream my $parser = Slim::Formats::RemoteMetadata->getParserFor( $url ); if ( $parser ) { if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( 'Trying metadata parser ' . Slim::Utils::PerlRunTime::realNameForCodeRef($parser) ); } - + my $handled = eval { $parser->( $client, $url, $metadata ) }; if ( $@ ) { my $name = main::DEBUGLOG ? Slim::Utils::PerlRunTime::realNameForCodeRef($parser) : 'unk'; @@ -146,15 +145,15 @@ sub parseMetadata { } return if $handled; } - + # Assume Icy metadata as first guess # BUG 15896 - treat as single line, as some stations add cr/lf to the song title field if ($metadata =~ (/StreamTitle=\'(.*?)\'(;|$)/s)) { - + main::DEBUGLOG && $log->is_debug && $log->debug("Icy metadata received: $metadata"); my $newTitle = Slim::Utils::Unicode::utf8decode_guess($1); - + # Some stations provide TuneIn enhanced metadata (TPID, itunesTrackID, etc.) in the title - remove it if ( $newTitle =~ /(.*?)- text="(.*?)"/ ) { $newTitle = "$1 - $2"; @@ -175,7 +174,7 @@ sub parseMetadata { ) /\U$1/xg; } - + # Check for an image URL in the metadata. my $artworkUrl; if ( $metadata =~ /StreamUrl=\'([^']+)\'/i ) { @@ -184,22 +183,22 @@ sub parseMetadata { $artworkUrl = undef; } } - + my $cb = sub { Slim::Music::Info::setCurrentTitle($url, $newTitle, $client); - + if ($artworkUrl) { my $cache = Slim::Utils::Cache->new(); $cache->set( "remote_image_$url", $artworkUrl, 3600 ); - + if ( my $song = $client->playingSong() ) { $song->pluginData( httpCover => $artworkUrl ); } - + main::DEBUGLOG && $directlog->debug("Updating stream artwork to $artworkUrl"); }; }; - + # Delay metadata according to buffer size if we already have metadata if ( $client->metaTitle() ) { Slim::Music::Info::setDelayedCallback( $client, $cb ); @@ -208,7 +207,7 @@ sub parseMetadata { $cb->(); } } - + # Check for Ogg metadata, which is formatted as a series of # 2-byte length/string pairs. elsif ( $metadata =~ /^Ogg(.+)/s ) { @@ -217,9 +216,9 @@ sub parseMetadata { while ( $comments ) { my $length = unpack 'n', substr( $comments, 0, 2, '' ); my $value = substr $comments, 0, $length, ''; - + main::DEBUGLOG && $directlog->is_debug && $directlog->debug("Ogg comment: $value"); - + # Bug 15896, a stream had CRLF in the metadata $metadata =~ s/\s*[\r\n]+\s*/; /g; @@ -234,15 +233,15 @@ sub parseMetadata { $meta->{title} = $1; } } - + # Re-use wmaMeta field my $song = $client->controller()->songStreamController()->song(); - + my $cb = sub { $song->pluginData( wmaMeta => $meta ); Slim::Music::Info::setCurrentTitle($url, $meta->{title}, $client) if $meta->{title}; }; - + # Delay metadata according to buffer size if we already have metadata if ( $song->pluginData('wmaMeta') ) { Slim::Music::Info::setDelayedCallback( $client, $cb, 'output-only' ); @@ -250,7 +249,7 @@ sub parseMetadata { else { $cb->(); } - + return; } @@ -259,7 +258,7 @@ sub parseMetadata { sub canDirectStream { my ($classOrSelf, $client, $url, $inType) = @_; - + # When synced, we don't direct stream so that the server can proxy a single # stream for all players if ( $client->isSynced(1) ) { @@ -280,7 +279,7 @@ sub canDirectStream { return 0; } } - + # Strip noscan info from URL $url =~ s/#slim:.+$//; @@ -319,7 +318,7 @@ sub sysread { } elsif ($metaPointer > $metaInterval) { main::DEBUGLOG && $log->debug("The shoutcast metadata overshot the interval."); - } + } } return $readLength; @@ -327,19 +326,19 @@ sub sysread { sub parseDirectHeaders { my ( $self, $client, $url, @headers ) = @_; - + my $isDebug = main::DEBUGLOG && $directlog->is_debug; - + # May get a track object if ( blessed($url) ) { $url = $url->url; } - + my ($title, $bitrate, $metaint, $redir, $contentType, $length, $body); my ($rangeLength, $startOffset); - + foreach my $header (@headers) { - + # Tidy up header to make no stray nulls or \n have been left by caller. $header =~ s/[\0]*$//; $header =~ s/\r/\n/g; @@ -348,57 +347,57 @@ sub parseDirectHeaders { $isDebug && $directlog->debug("header-ds: $header"); if ($header =~ /^(?:ic[ey]-name|x-audiocast-name):\s*(.+)/i) { - + $title = Slim::Utils::Unicode::utf8decode_guess($1); } - + elsif ($header =~ /^(?:icy-br|x-audiocast-bitrate):\s*(.+)/i) { $bitrate = $1; $bitrate *= 1000 if $bitrate < 1000; } - + elsif ($header =~ /^icy-metaint:\s*(.+)/i) { $metaint = $1; } - + elsif ($header =~ /^Location:\s*(.*)/i) { $redir = $1; } - + elsif ($header =~ /^Content-Type:\s*([^;\n]*)/i) { $contentType = $1; } - + elsif ($header =~ /^Content-Length:\s*(.*)/i) { $length = $1; } - + elsif ($header =~ m%^Content-Range:\s+bytes\s+(\d+)-(\d+)/(\d+)%i) { $rangeLength = $3; $startOffset = $1; } - + # mp3tunes metadata, this is a bit of hack but creating # an mp3tunes protocol handler is overkill elsif ( $url =~ /mp3tunes\.com/ && $header =~ /^X-Locker-Info:\s*(.+)/i ) { Slim::Plugin::MP3tunes::Plugin->setLockerInfo( $client, $url, $1 ); } } - + # Content-Range: has predecence over Content-Length: if ($rangeLength) { $length = $rangeLength; } - + my $song = ${*self}{'song'} if blessed $self; - + if (!$song && $client->controller()->songStreamController()) { $song = $client->controller()->songStreamController()->song(); } - + if ($song && $length) { my $seekdata = $song->seekdata(); - + if ($startOffset && $seekdata && $seekdata->{restartOffset} && $seekdata->{sourceStreamOffset} && $startOffset > $seekdata->{sourceStreamOffset}) { @@ -408,15 +407,15 @@ sub parseDirectHeaders { my $streamLength = $length; $streamLength -= $startOffset if $startOffset; $song->streamLength($streamLength); - + # However we got here, we want to know that we did not start at the beginning, if possible if ($startOffset) { - - + + # Assume saved duration is more accurate that by calculating from length and bitrate my $duration = Slim::Music::Info::getDuration($url); $duration ||= $length * 8 / $bitrate if $bitrate; - + if ($duration) { main::INFOLOG && $directlog->info("Setting startOffest based on Content-Range to ", $duration * ($startOffset/$length)); $song->startOffset($duration * ($startOffset/$length)); @@ -425,14 +424,14 @@ sub parseDirectHeaders { } $contentType = Slim::Music::Info::mimeToType($contentType); - + if ( !$contentType ) { # Bugs 7225, 7423 # Default contentType to mp3 as some servers don't send the type # or send an invalid type we don't include in types.conf $contentType = 'mp3'; } - + return ($title, $bitrate, $metaint, $redir, $contentType, $length, $body); } @@ -451,23 +450,24 @@ sub parseHeaders { my $self = shift; my $url = $self->url; my $client = $self->client; - + my $isOgf = Slim::Music::Info::contentType( $url ) eq 'ogf'; + my ($title, $bitrate, $metaint, $redir, $contentType, $length, $body) = $self->parseDirectHeaders($client, $url, @_); - if ($contentType) { + if ($contentType && !$isOgf) { if (($contentType =~ /text/i) && !($contentType =~ /text\/xml/i)) { # webservers often lie about playlists. This will # make it guess from the suffix. (unless text/xml) $contentType = ''; } - + ${*$self}{'contentType'} = $contentType; Slim::Music::Info::setContentType( $url, $contentType ); } - + ${*$self}{'redirect'} = $redir; - + ${*$self}{'contentLength'} = $length if $length; ${*$self}{'song'}->isLive($length ? 0 : 1) if !$redir; @@ -475,7 +475,7 @@ sub parseHeaders { if ( $title ) { main::INFOLOG && $log->is_info && $log->info( "Setting new title for $url, $title" ); Slim::Music::Info::setCurrentTitle( $url, $title ); - + # Bug 7979, Only update the database title if this item doesn't already have a title my $curTitle = Slim::Music::Info::title($url); if ( !$curTitle || $curTitle =~ /^(?:http|mms)/ ) { @@ -483,29 +483,29 @@ sub parseHeaders { } } - if ($bitrate) { + if ($bitrate && !$isOgf) { main::INFOLOG && $log->is_info && $log->info(sprintf("Bitrate for %s set to %d", $self->infoUrl, $bitrate, )); - + ${*$self}{'bitrate'} = $bitrate; Slim::Music::Info::setBitrate( $self->infoUrl, $bitrate ); } elsif ( !$self->bitrate ) { # Bitrate may have been set in Scanner by reading the mp3 stream $bitrate = ${*$self}{'bitrate'} = Slim::Music::Info::getBitrate( $url ); } - - + + if ($metaint) { ${*$self}{'metaInterval'} = $metaint; ${*$self}{'metaPointer'} = 0; } - + # See if we have an existing track object with duration info for this stream. if ( my $secs = Slim::Music::Info::getDuration( $url ) ) { - + # Display progress bar $client->streamingProgressBar( { 'url' => $url, @@ -513,8 +513,8 @@ sub parseHeaders { } ); } else { - - if ( $bitrate > 0 && defined $self->contentLength && $self->contentLength > 0 ) { + + if ( $bitrate && $bitrate > 0 && defined $self->contentLength && $self->contentLength > 0 ) { # if we know the bitrate and length of a stream, display a progress bar if ( $bitrate < 1000 ) { ${*$self}{'bitrate'} *= 1000; @@ -526,7 +526,7 @@ sub parseHeaders { } ); } } - + # Bug 6482, refresh the cached Track object in the client playlist from the database # so it picks up any changed data such as title, bitrate, etc Slim::Player::Playlist::refreshTrack( $client, $url ); @@ -548,10 +548,10 @@ sub requestString { my $seekdata = shift; my ($server, $port, $path, $user, $password) = Slim::Utils::Misc::crackURL($url); - + # Use full path for proxy servers my $proxy = $prefs->get('webproxy'); - + if ( $proxy && $server !~ /(?:localhost|127.0.0.1)/ ) { $path = "http://$server:$port$path"; } @@ -563,7 +563,7 @@ sub requestString { # According to the spec, http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html # The port is optional if it's 80, so follow that rule. my $host = $port == 80 ? $server : "$server:$port"; - + # Special case, for the fallback-alarm, disable Icy Metadata, or our own # server will try and send it my $want_icy = 1; @@ -581,17 +581,17 @@ sub requestString { "Connection: close", "Host: $host", )); - + if (defined($user) && defined($password)) { $request .= $CRLF . "Authorization: Basic " . MIME::Base64::encode_base64($user . ":" . $password,''); } - + $client->songBytes(0) if $client; # If seeking, add Range header if ($client && $seekdata) { - $request .= $CRLF . 'Range: bytes=' . int( $seekdata->{sourceStreamOffset} + $seekdata->{restartOffset}) . '-'; - + $request .= $CRLF . 'Range: bytes=' . int( ($seekdata->{sourceStreamOffset} || 0) + ($seekdata->{restartOffset} || 0) ) . '-'; + if (defined $seekdata->{timeOffset}) { # Fix progress bar $client->playingSong()->startOffset($seekdata->{timeOffset}); @@ -611,13 +611,13 @@ sub requestString { } else { $request .= $CRLF . $CRLF; } - + # Bug 5858, add cookies to the request my $request_object = HTTP::Request->parse($request); $request_object->uri($url); Slim::Networking::Async::HTTP::cookie_jar->add_cookie_header( $request_object ); $request_object->uri($path); - + # Bug 9709, strip long cookies from the request $request_object->headers->scan( sub { if ( $_[0] eq 'Cookie' ) { @@ -626,34 +626,34 @@ sub requestString { } } } ); - - $request = $request_object->as_string( $CRLF ); + + $request = $request_object->as_string( $CRLF ); return $request; } sub scanUrl { my ( $class, $url, $args ) = @_; - + Slim::Utils::Scanner::Remote->scanURL($url, $args); } # Allow mp3tunes tracks to be scrobbled sub audioScrobblerSource { my ( $class, $client, $url ) = @_; - + if ( $url =~ /mp3tunes\.com/ ) { # Scrobble mp3tunes as 'chosen by user' content return 'P'; } - + # R (radio source) return 'R'; } sub getMetadataFor { my ( $class, $client, $url, $forceCurrent ) = @_; - + # Check for an alternate metadata provider for this URL my $provider = Slim::Formats::RemoteMetadata->getProviderFor($url); if ( $provider ) { @@ -666,7 +666,7 @@ sub getMetadataFor { return $metadata; } } - + # Check for parsed WMA metadata, this is here because WMA may # use HTTP protocol handler if ( my $song = $client->playingSong() ) { @@ -684,13 +684,13 @@ sub getMetadataFor { if ( $meta->{cover} ) { $data->{cover} = $meta->{cover}; } - + if ( scalar keys %{$data} ) { return $data; } } } - + my ($artist, $title); # Radio tracks, return artist and title if the metadata looks like Artist - Title if ( my $currentTitle = Slim::Music::Info::getCurrentTitle( $client, $url ) ) { @@ -703,37 +703,37 @@ sub getMetadataFor { $title = $currentTitle; } } - + # Remember playlist URL my $playlistURL = $url; - + # Check for radio URLs with cached covers my $cache = Slim::Utils::Cache->new(); my $cover = $cache->get( "remote_image_$url" ); - + # Item may be a playlist, so get the real URL playing if ( Slim::Music::Info::isPlaylist($url) ) { if (my $cur = $client->currentTrackForUrl($url)) { $url = $cur->url; } } - + # Remote streams may include ID3 tags with embedded artwork # Example: http://downloads.bbc.co.uk/podcasts/radio4/excessbag/excessbag_20080426-1217.mp3 my $track = Slim::Schema->objectForUrl( { url => $url, } ); - + return {} unless $track; - + if ( $track->cover ) { # XXX should remote tracks use coverid? $cover = '/music/' . $track->id . '/cover.jpg'; } - + $artist ||= $track->artistName; - - if ( $url =~ /archive\.org/ || $url =~ m|squeezenetwork\.com.+/lma/| ) { + + if ( $url =~ /archive\.org/ || $url =~ m|mysqueezebox\.com.+/lma/| ) { if ( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::LMA::Plugin') ) { my $icon = Slim::Plugin::LMA::Plugin->_pluginDataFor('icon'); return { @@ -744,18 +744,18 @@ sub getMetadataFor { }; } } - else { + else { - if ( (my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url)) !~ /^(?:$class|Slim::Player::Protocols::MMS)$/ ) { + if ( (my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url)) !~ /^(?:$class|Slim::Player::Protocols::MMS|Slim::Player::Protocols::HTTPS?)$/ ) { if ( $handler && $handler->can('getMetadataFor') ) { return $handler->getMetadataFor( $client, $url ); } - } + } + + my $type = uc( $track->content_type || '' ) . ' ' . Slim::Utils::Strings::cstring($client, 'RADIO'); - my $type = uc( $track->content_type ) . ' ' . Slim::Utils::Strings::cstring($client, 'RADIO'); - my $icon = $class->getIcon($url, 'no fallback artwork') || $class->getIcon($playlistURL); - + return { artist => $artist, title => $title, @@ -766,7 +766,7 @@ sub getMetadataFor { cover => $cover || $icon, }; } - + return {}; } @@ -784,33 +784,33 @@ sub getIcon { sub canSeek { my ( $class, $client, $song ) = @_; - + $client = $client->master(); - + # Can only seek if bitrate and duration are known my $bitrate = $song->bitrate(); my $seconds = $song->duration(); - - if ( !$bitrate || !$seconds ) { + + if ( !$bitrate || !$seconds || $song->streamformat =~ /(pcm|wav|aif)/ ) { #$log->debug( "bitrate: $bitrate, duration: $seconds" ); #$log->debug( "Unknown bitrate or duration, seek disabled" ); return 0; } - + return 1; } sub canSeekError { my ( $class, $client, $song ) = @_; - + my $url = $song->currentTrack()->url; - + my $ct = Slim::Music::Info::contentType($url); - + if ( $ct ne 'mp3' ) { return ( 'SEEK_ERROR_TYPE_NOT_SUPPORTED', $ct ); - } - + } + if ( !$song->bitrate() ) { main::INFOLOG && $log->info("bitrate unknown for: " . $url); return 'SEEK_ERROR_MP3_UNKNOWN_BITRATE'; @@ -818,20 +818,20 @@ sub canSeekError { elsif ( !$song->duration() ) { return 'SEEK_ERROR_MP3_UNKNOWN_DURATION'; } - + return 'SEEK_ERROR_MP3'; } sub getSeekData { my ( $class, $client, $song, $newtime ) = @_; - + # Determine byte offset and song length in bytes my $bitrate = $song->bitrate() || return; - + $bitrate /= 1000; - + main::INFOLOG && $log->info( "Trying to seek $newtime seconds into $bitrate kbps" ); - + return { sourceStreamOffset => ( ( $bitrate * 1000 ) / 8 ) * $newtime, timeOffset => $newtime, @@ -840,9 +840,9 @@ sub getSeekData { sub getSeekDataByPosition { my ($class, $client, $song, $bytesReceived) = @_; - + my $seekdata = $song->seekdata() || {}; - + return {%$seekdata, restartOffset => $bytesReceived}; } diff --git a/Slim/Player/Protocols/HTTPS.pm b/Slim/Player/Protocols/HTTPS.pm new file mode 100644 index 00000000000..7d937f41aba --- /dev/null +++ b/Slim/Player/Protocols/HTTPS.pm @@ -0,0 +1,110 @@ +package Slim::Player::Protocols::HTTPS; + +use base qw(IO::Socket::SSL Slim::Player::Protocols::HTTP); + +use Slim::Utils::Errno; +use Slim::Utils::Log; +use Slim::Utils::Prefs; + +my $log = logger('player.streaming.remote'); +my $prefs = preferences('server'); + +sub new { + my $class = shift; + my $args = shift; + my $url = $args->{'url'} || ''; + + my ($server, $port, $path) = Slim::Utils::Misc::crackURL($url); + + if (!$server || !$port) { + + logError("Couldn't find server or port in url: [$url]"); + return; + } + + my $timeout = $args->{'timeout'} || $prefs->get('remotestreamtimeout'); + + main::INFOLOG && $log->is_info && $log->info("Opening connection to $url: [$server on port $port with path $path with timeout $timeout]"); + + my $sock = $class->SUPER::new( + Timeout => $timeout, + PeerAddr => $server, + PeerPort => $port, + SSL_startHandshake => 1, + SSL_verify_mode => Net::SSLeay::VERIFY_NONE() # SSL_VERIFY_NONE isn't recognized on some platforms?!?, and 0x00 isn't always "right" + ) or do { + + $log->error("Couldn't create socket binding to $main::localStreamAddr with timeout: $timeout - $!"); + return undef; + }; + + if (defined($sock)) { + ${*$sock}{'client'} = $args->{'client'}; + ${*$sock}{'url'} = $args->{'url'}; + ${*$sock}{'song'} = $args->{'song'}; + + # store a IO::Select object in ourself. + # used for non blocking I/O + ${*$sock}{'_sel'} = IO::Select->new($sock); + } + + return $sock->request($args); +} + +# Check whether the current player can stream HTTPS or not +sub canDirectStream { + my $self = shift; + my ($client) = @_; + + if ( $client->canHTTPS ) { + return $self->SUPER::canDirectStream(@_); + } + + return 0; +} + +# as we are inheriting from IO::Socket::SSL first, we have to re-implement Slim::Player::Protocols::HTTP->sysread here +sub sysread { + my $self = $_[0]; + my $chunkSize = $_[2]; + + my $metaInterval = ${*$self}{'metaInterval'}; + my $metaPointer = ${*$self}{'metaPointer'}; + + if ($chunkSize && $metaInterval && ($metaPointer + $chunkSize) > $metaInterval && ($metaInterval - $metaPointer) > 0) { + + $chunkSize = $metaInterval - $metaPointer; + + # This is very verbose... + #$log->debug("Reduced chunksize to $chunkSize for metadata"); + } + + my $readLength = $self->SUPER::sysread($_[1], $chunkSize); + + if ($metaInterval && $readLength) { + + $metaPointer += $readLength; + ${*$self}{'metaPointer'} = $metaPointer; + + # handle instream metadata for shoutcast/icecast + if ($metaPointer == $metaInterval) { + + $self->readMetaData(); + + ${*$self}{'metaPointer'} = 0; + + } elsif ($metaPointer > $metaInterval) { + + main::DEBUGLOG && $log->debug("The shoutcast metadata overshot the interval."); + } + } + + if (main::ISWINDOWS && !$readLength) { + $! = EWOULDBLOCK; + } + + return $readLength; +} + + +1; diff --git a/Slim/Player/Protocols/MMS.pm b/Slim/Player/Protocols/MMS.pm index 4910b16e5e8..0debcf49dba 100644 --- a/Slim/Player/Protocols/MMS.pm +++ b/Slim/Player/Protocols/MMS.pm @@ -1,8 +1,7 @@ package Slim::Player::Protocols::MMS; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech, Vidur Apparao. +# Logitech Media Server Copyright 2001-2020 Logitech, Vidur Apparao. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Protocols/Volatile.pm b/Slim/Player/Protocols/Volatile.pm index 8d873bb6440..9220baee5b4 100644 --- a/Slim/Player/Protocols/Volatile.pm +++ b/Slim/Player/Protocols/Volatile.pm @@ -1,6 +1,6 @@ package Slim::Player::Protocols::Volatile; -# Logitech Media Server Copyright 2001-2011 Logitech, Vidur Apparao. +# Logitech Media Server Copyright 2001-2020 Logitech, Vidur Apparao. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -12,6 +12,7 @@ use strict; use Slim::Music::Artwork; use Slim::Utils::Log; +use Slim::Utils::Strings qw(cstring); use base qw(Slim::Player::Protocols::File); sub isRemote { 1 } @@ -35,7 +36,7 @@ sub getMetadataFor { my $attributes = Slim::Formats->readTags( $path ); # make sure we have a value for artist, or we'll end up scanning the file over and over again - $attributes->{ARTIST} = $client->string('NO_ARTIST') unless defined $attributes->{ARTIST}; + $attributes->{ARTIST} = cstring($client, 'NO_ARTIST') unless defined $attributes->{ARTIST}; $track->setAttributes($attributes) if $attributes && keys %$attributes; diff --git a/Slim/Player/Receiver.pm b/Slim/Player/Receiver.pm index dff6af3dadc..edefa5e25b2 100644 --- a/Slim/Player/Receiver.pm +++ b/Slim/Player/Receiver.pm @@ -1,6 +1,6 @@ package Slim::Player::Receiver; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/ReplayGain.pm b/Slim/Player/ReplayGain.pm index e8e2feabffa..e7dbf160199 100644 --- a/Slim/Player/ReplayGain.pm +++ b/Slim/Player/ReplayGain.pm @@ -1,8 +1,7 @@ package Slim::Player::ReplayGain; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/SB1SliMP3Sync.pm b/Slim/Player/SB1SliMP3Sync.pm index 2626e735a6c..eedfd636b7e 100644 --- a/Slim/Player/SB1SliMP3Sync.pm +++ b/Slim/Player/SB1SliMP3Sync.pm @@ -1,6 +1,6 @@ package Slim::Player::SB1SliMP3Sync; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -10,7 +10,6 @@ package Slim::Player::SB1SliMP3Sync; # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # -# $Id$ # use strict; @@ -143,7 +142,7 @@ sub _findTimeForOffset { } } - return unless ( @{$frames} && @{$frames} > 1 ); + return unless ( $frames && @{$frames} && @{$frames} > 1 ); my ($i, $j, $k) = (0, @{$frames} - 1); diff --git a/Slim/Player/SLIMP3.pm b/Slim/Player/SLIMP3.pm index 79d7134bdf3..a0a9d3bc2f6 100644 --- a/Slim/Player/SLIMP3.pm +++ b/Slim/Player/SLIMP3.pm @@ -1,4 +1,4 @@ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/SoftSqueeze.pm b/Slim/Player/SoftSqueeze.pm index 6b2a6df347f..3caec19ea2e 100644 --- a/Slim/Player/SoftSqueeze.pm +++ b/Slim/Player/SoftSqueeze.pm @@ -1,6 +1,6 @@ package Slim::Player::SoftSqueeze; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Song.pm b/Slim/Player/Song.pm index 4585a7b789e..07807f5cdfd 100644 --- a/Slim/Player/Song.pm +++ b/Slim/Player/Song.pm @@ -1,8 +1,7 @@ package Slim::Player::Song; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -46,7 +45,7 @@ my @_playlistCloneAttributes = qw( streamUrl owner _playlist _scanDone - + _pluginData wmaMetadataStream wmaMetaData scanData ); @@ -57,19 +56,19 @@ my @_playlistCloneAttributes = qw( __PACKAGE__->mk_accessor('rw', @_playlistCloneAttributes, - + qw( _status - + startOffset streamLength seekdata initialAudioBlock _canSeek _canSeekError - + _duration _bitrate _streambitrate _streamFormat _transcoded directstream - + samplerate samplesize channels totalbytes offset blockalign isLive - + retryData ), ); @@ -79,12 +78,12 @@ sub new { my ($class, $owner, $index, $seekdata) = @_; my $client = $owner->master(); - + my $objOrUrl = Slim::Player::Playlist::song($client, $index) || return undef; - + # Bug: 3390 - reload the track if it's changed. my $url = blessed($objOrUrl) && $objOrUrl->can('url') ? $objOrUrl->url : $objOrUrl; - + my $track = Slim::Schema->objectForUrl({ 'url' => $url, 'readTags' => 1 @@ -102,7 +101,7 @@ sub new { return undef; } } - + $url = $track->url; main::INFOLOG && $log->info("index $index -> $url"); @@ -120,9 +119,9 @@ sub new { logError("Could not find handler for $url!"); return undef; } - + my $self = $class->SUPER::new; - + $self->init_accessor( index => $index, _status => STATUS_READY, @@ -134,9 +133,9 @@ sub new { _track => $track, streamUrl => $url, # May get updated later, either here or in handler ); - + $self->seekdata($seekdata) if $seekdata; - + if ($handler->can('isRepeatingStream')) { my $type = $handler->isRepeatingStream($self); if ($type > 2) { @@ -150,7 +149,7 @@ sub new { if (main::DEBUGLOG && $log->is_debug) { $log->debug("live=$_liveCount"); } - + return $self; } @@ -164,20 +163,20 @@ sub DESTROY { sub clonePlaylistSong { my ($old) = @_; - + assert($old->isPlaylist()); - + my $new = (ref $old)->SUPER::new; - + $new->init_accessor( _status => STATUS_READY, startOffset => 0, ); - + foreach ('handler', @_playlistCloneAttributes) { $new->init_accessor($_ => $old->$_()); } - + $_liveCount++; if (main::DEBUGLOG && $log->is_debug) { $log->debug("live=$_liveCount"); @@ -195,18 +194,18 @@ sub resetSeekdata { sub _getNextPlaylistTrack { my ($self) = @_; - + if ($self->_playlist() >= 2) { # leave it to the protocol handler in getNextTrack() - + # Old handlers expect this if ($self->_playlist() == 2) { $self->_currentTrack($self->_track()); } - + return $self->_track(); } - + # Get the next good audio track my $playlist = Slim::Schema->objectForUrl( {url => $self->_track()->url, playlist => 1} ); main::DEBUGLOG && $log->is_debug && $log->debug( "Getting next audio URL from playlist (after " . ($self->_currentTrack() ? $self->_currentTrack()->url : '') . ")" ); @@ -225,7 +224,7 @@ sub getNextSong { my ($self, $successCb, $failCb) = @_; my $handler = $self->currentTrackHandler(); - + main::INFOLOG && $log->info($self->currentTrack()->url); # if (playlist and no-track and (scanned or not scannable)) { @@ -239,11 +238,11 @@ sub getNextSong { } $handler = $self->currentTrackHandler(); } - + my $track = $self->currentTrack(); my $url = $track->url; my $client = $self->master(); - + # If we have (a) a scannable playlist track, # or (b) a scannable track that is not yet scanned and could be a playlist ... if ($handler->can('scanUrl') && !$self->_scanDone()) { @@ -254,17 +253,18 @@ sub getNextSong { song => $self, cb => sub { my ( $newTrack, $error ) = @_; - + if ($newTrack) { - + if ($track != $newTrack) { - + if ($self->_track() == $track) { # Update of original track, by playlist or redirection $self->_track($newTrack); - + $self->_currentTrackHandler(Slim::Player::ProtocolHandlers->handlerForURL($newTrack->url)); + main::INFOLOG && $log->info("Track updated by scan: $url -> " . $newTrack->url); - + # Replace the item on the playlist so it has the new track/URL my $i = 0; for my $item ( @{ Slim::Player::Playlist::playList($client) } ) { @@ -279,14 +279,14 @@ sub getNextSong { # The current, playlist track got updated, maybe by redirection # Probably should not happen as redirection should have been # resolved during recursive scan of playlist. - + # Cannot update $self->_currentTrack() as would mess up playlist traversal $log->warn("Unexpected update of playlist track: $url -> " . $newTrack->url); } - + $track = $newTrack; } - + # maybe we just found or scanned a playlist if (!$self->_currentTrack() && !$self->_playlist()) { $self->_playlist(Slim::Music::Info::isPlaylist($track, $track->content_type) ? 1 : 0); @@ -305,9 +305,9 @@ sub getNextSong { # Notify of failure via cant_open, this is used to pick # up the failure for automatic RadioTime reporting Slim::Control::Request::notifyFromArray( $client, [ 'playlist', 'cant_open', $url, $error ] ); - + $error ||= 'PROBLEM_OPENING_REMOTE_URL'; - + $failCb->($error, $url); } }, @@ -347,21 +347,21 @@ my %streamFormatMap = ( sub open { my ($self, $seekdata) = @_; - + my $handler = $self->currentTrackHandler(); my $client = $self->master(); my $track = $self->currentTrack(); assert($track); my $url = $track->url; - + # Reset seekOffset - handlers will set this if necessary $self->startOffset(0); - + # Restart direct-stream $self->directstream(0); - + main::INFOLOG && $log->info($url); - + $self->seekdata($seekdata) if $seekdata; my $sock; my $format = Slim::Music::Info::contentType($track); @@ -369,12 +369,12 @@ sub open { if ($handler->can('formatOverride')) { $format = $handler->formatOverride($self); } - + # get transcoding command & stream-mode # IF command == '-' AND canDirectStream THEN # direct stream # ELSE - # ASSERT stream-mode == 'I' OR command != '-' + # ASSERT stream-mode == 'I' OR command != '-' # # IF stream-mode == 'I' OR handler-does-transcoding THEN # open stream @@ -383,46 +383,46 @@ sub open { # add transcoding pipeline # ENDIF # ENDIF - + main::INFOLOG && $log->info("seek=", ($self->seekdata() ? 'true' : 'false'), ' time=', ($self->seekdata() ? $self->seekdata()->{'timeOffset'} : 0), ' canSeek=', $self->canSeek()); - + my $transcoder; my $error; - + if (main::TRANSCODING) { my $wantTranscoderSeek = $self->seekdata() && $self->seekdata()->{'timeOffset'} && $self->canSeek() == 2; my @wantOptions; push (@wantOptions, 'T') if ($wantTranscoderSeek); - + my @streamFormats; push (@streamFormats, 'I') if (! $wantTranscoderSeek); - + push @streamFormats, ($handler->isRemote && !Slim::Music::Info::isVolatile($handler) ? 'R' : 'F'); - + ($transcoder, $error) = Slim::Player::TranscodingHelper::getConvertCommand2( $self, $format, \@streamFormats, [], \@wantOptions); - + if (! $transcoder) { logError("Couldn't create command line for $format playback for [$url]"); return (undef, ($error || 'PROBLEM_CONVERT_FILE'), $url); } elsif (main::INFOLOG && $log->is_info) { $log->info("Transcoder: streamMode=", $transcoder->{'streamMode'}, ", streamformat=", $transcoder->{'streamformat'}); } - + if ($wantTranscoderSeek && (grep(/T/, @{$transcoder->{'usedCapabilities'}}))) { $transcoder->{'start'} = $self->startOffset($self->seekdata()->{'timeOffset'}); } } else { require Slim::Player::CapabilitiesHelper; - + # Set the correct format for WAV/AAC playback if ( exists $streamFormatMap{$format} ) { $format = $streamFormatMap{$format}; } - + # Is format supported by all players? if (!grep {$_ eq $format} Slim::Player::CapabilitiesHelper::supportedFormats($client)) { $error = 'PROBLEM_CONVERT_FILE'; @@ -436,7 +436,7 @@ sub open { logError("$error [$url]"); return (undef, $error, $url); } - + $transcoder = { command => '-', streamformat => $format, @@ -444,53 +444,54 @@ sub open { rateLimit => 0, }; } - + # TODO work this out for each player in the sync-group my $directUrl; - if ($transcoder->{'command'} eq '-' && ($directUrl = $client->canDirectStream($url, $self))) { + # Make sure for direct mode that if transcode rule is identity, codec is _really_ supported (e.g. wav vs pcm) + if ($transcoder->{'command'} eq '-' && ($directUrl = $client->canDirectStream($url, $self)) && grep {$_ eq $format} Slim::Player::CapabilitiesHelper::supportedFormats($client)) { main::INFOLOG && $log->info( "URL supports direct streaming [$url->$directUrl]" ); $self->directstream(1); $self->streamUrl($directUrl); } - + else { my $handlerWillTranscode = $transcoder->{'command'} ne '-' && $handler->can('canHandleTranscode') && $handler->canHandleTranscode($self); if ($transcoder->{'streamMode'} eq 'I' || $handlerWillTranscode) { main::INFOLOG && $log->info("Opening stream (no direct streaming) using $handler [$url]"); - + $sock = $handler->new({ url => $url, # it is just easier if we always include the URL here client => $client, song => $self, transcoder => $transcoder, }); - + if (!$sock) { logWarning("stream failed to open [$url]."); $self->setStatus(STATUS_FAILED); return (undef, $self->isRemote() ? 'PROBLEM_CONNECTING' : 'PROBLEM_OPENING', $url); } - + my $contentType = Slim::Music::Info::mimeToType($sock->contentType) || $sock->contentType; - + # if it's an audio stream, try to stream, # either directly, or via transcoding. if (Slim::Music::Info::isSong($track, $contentType)) { - + main::INFOLOG && $log->info("URL is a song (audio): $url, type=$contentType"); - + if ($sock->opened() && !defined(Slim::Utils::Network::blocking($sock, 0))) { logError("Can't set nonblocking for url: [$url]"); return (undef, 'PROBLEM_OPENING', $url); } - + if ($handlerWillTranscode) { $self->_transcoded(1); $self->_streambitrate($sock->getStreamBitrate($transcoder->{'rateLimit'}) || 0); } - + # If the protocol handler has the bitrate set use this if ($sock->can('bitrate') && $sock->bitrate) { $self->_bitrate($sock->bitrate); @@ -498,37 +499,37 @@ sub open { } # if it's one of our playlists, parse it... elsif (Slim::Music::Info::isList($track, $contentType)) { - + # handle the case that we've actually # got a playlist in the list, rather # than a stream. - + # parse out the list my @items = Slim::Formats::Playlists->parseList($url, $sock); - + # hack to preserve the title of a song redirected through a playlist if (scalar(@items) == 1 && $items[0] && defined($track->title)) { Slim::Music::Info::setTitle($items[0], $track->title); } - + # close the socket $sock->close(); $sock = undef; - + Slim::Player::Source::explodeSong($client, \@items); - + my $new = $self->new ($self->owner(), $self->index()); %$self = %$new; - + # try to open the first item in the list, if there is one. $self->getNextSong ( sub {return $self->open();}, # success sub {return(undef, @_);} # fail ); - + } else { logWarning("Don't know how to handle content for [$url] type: $contentType"); - + $sock->close(); $sock = undef; @@ -540,23 +541,23 @@ sub open { if (main::TRANSCODING) { if ($transcoder->{'command'} ne '-' && ! $handlerWillTranscode) { # Need to transcode - + my $quality = $prefs->client($client)->get('lameQuality'); - + # use a pipeline on windows when remote as we need socketwrapper to ensure we get non blocking IO my $usepipe = (defined $sock || (main::ISWINDOWS && $handler->isRemote)) ? 1 : undef; - + my $command = Slim::Player::TranscodingHelper::tokenizeConvertCommand2( $transcoder, $sock ? '-' : $track->path, $self->streamUrl(), $usepipe, $quality ); - + if (!defined($command)) { logError("Couldn't create command line for $format playback for [$self->streamUrl()]"); return (undef, 'PROBLEM_CONVERT_FILE', $url); } - + main::INFOLOG && $log->info('Tokenized command: ', Slim::Utils::Unicode::utf8decode_locale($command)); - + my $pipeline; # Bug 10451: only use Pipeline when really necessary @@ -570,51 +571,44 @@ sub open { Win32::SetChildShowWindow(0); $pipeline = FileHandle->new; my $pid = $pipeline->open($command); - - # XXX Bug 15650, this sets the priority of the cmd.exe process but not the actual - # transcoder process(es). - my $handle; - if ( Win32::Process::Open( $handle, $pid, 0 ) ) { - $handle->SetPriorityClass( Slim::Utils::OS::Win32::getPriorityClass() || Win32::Process::NORMAL_PRIORITY_CLASS() ); - } - + Win32::SetChildShowWindow(); } else { $pipeline = FileHandle->new($command); } - + if ($pipeline && $pipeline->opened() && !defined(Slim::Utils::Network::blocking($pipeline, 0))) { logError("Can't set nonblocking for url: [$url]"); return (undef, 'PROBLEM_OPENING', $url); } } - + if (!defined($pipeline)) { logError("$!: While creating conversion pipeline for: ", $self->streamUrl()); $sock->close() if $sock; return (undef, 'PROBLEM_CONVERT_STREAM', $url); } - + $sock = $pipeline; - + $self->_transcoded(1); - + $self->_streambitrate(guessBitrateFromFormat($transcoder->{'streamformat'}, $transcoder->{'rateLimit'}) || 0); } } # ENDIF main::TRANSCODING - + $client->remoteStreamStartTime(Time::HiRes::time()); $client->pauseTime(0); } my $streamController; - + ###################### # make sure the filehandle was actually set if ($sock || $self->directstream()) { if ($sock && $sock->opened()) { - + # binmode() can mess with the file position but, since we cannot # rely on all possible protocol handlers to have set binmode, # we need to try to preserve the seek position if it is set. @@ -637,7 +631,7 @@ sub open { $persistent->update; } } - + $self->_streamFormat($transcoder->{'streamformat'}); $client->streamformat($self->_streamFormat()); # XXX legacy @@ -652,16 +646,16 @@ sub open { Slim::Control::Request::notifyFromArray($client, ['playlist', 'open', $url]); $self->setStatus(STATUS_STREAMING); - + $client->metaTitle(undef); - + return $streamController; } # Static method sub guessBitrateFromFormat { my ($format, $maxRate) = @_; - + # Hack to set up stream bitrate for songTime for SliMP3/SB1 # Also used when rebuffering, etc. if ($format eq 'mp3') { @@ -677,17 +671,17 @@ sub guessBitrateFromFormat { sub pluginData { my ( $self, $key, $value ) = @_; - + my $ret; - + if ( !defined $self->_pluginData() ) { $self->_pluginData({}); } - + if ( !defined $key ) { return $self->_pluginData(); } - + if ( ref $key eq 'HASH' ) { # Assign an entire hash to pluginData $ret = $self->_pluginData($key); @@ -696,10 +690,10 @@ sub pluginData { if ( defined $value ) { $self->_pluginData()->{$key} = $value; } - + $ret = $self->_pluginData()->{$key}; } - + return $ret; } @@ -716,17 +710,17 @@ sub status {return $_[0]->_status();} sub getSeekDataByPosition { my ($self, $bytesReceived) = @_; - + return undef if $self->_transcoded(); - + my $streamLength = $self->streamLength(); - + if ($streamLength && $bytesReceived >= $streamLength) { return {streamComplete => 1}; } - + my $handler = $self->currentTrackHandler(); - + if ($handler->can('getSeekDataByPosition')) { return $handler->getSeekDataByPosition($self->master(), $self, $bytesReceived); } else { @@ -736,9 +730,9 @@ sub getSeekDataByPosition { sub getSeekData { my ($self, $newtime) = @_; - + my $handler = $self->currentTrackHandler(); - + if ($handler->can('getSeekData')) { return $handler->getSeekData($self->master(), $self, $newtime); } else { @@ -748,7 +742,7 @@ sub getSeekData { sub bitrate { my $self = shift; - + if (scalar @_) { return $self->_bitrate($_[0]); } @@ -757,7 +751,7 @@ sub bitrate { sub duration { my $self = shift; - + if (scalar @_) { return $self->_duration($_[0]); } @@ -779,7 +773,7 @@ sub streambitrate { sub setStatus { my ($self, $status) = @_; $self->_status($status); - + # Bug 11156 - we reset the seekability evaluation here in case we now know more after # parsing the actual stream headers or the background sanner has had time to finish. $self->_canSeek(undef); @@ -787,23 +781,23 @@ sub setStatus { sub canSeek { my $self = shift; - + my $canSeek = $self->canDoSeek(); - + return $canSeek if $canSeek; - + return wantarray ? ( $canSeek, @{$self->_canSeekError()} ) : $canSeek; } sub canDoSeek { my $self = shift; - + return $self->_canSeek() if (defined $self->_canSeek()); - + my $handler = $self->currentTrackHandler(); - + if (!main::TRANSCODING) { - + if ( $handler->can('canSeek') && $handler->canSeek( $self->master(), $self )) { return $self->_canSeek(1); } else { @@ -819,26 +813,27 @@ sub canDoSeek { if ($handler->can('canSeek')) { if ($handler->canSeek( $self->master(), $self )) { + return $self->_canSeek(2) if $handler->can('canTranscodeSeek') && $handler->canTranscodeSeek(); return $self->_canSeek(1) if $handler->isRemote() && !Slim::Music::Info::isVolatile($handler); - + # If dealing with local file and transcoding then best let transcoder seek if it can - + # First see how we would stream without seeking question my $transcoder = Slim::Player::TranscodingHelper::getConvertCommand2( $self, Slim::Music::Info::contentType($self->currentTrack), ['I', 'F'], [], []); - + if (! $transcoder) { $self->_canSeekError([ 'SEEK_ERROR_TRANSCODED' ]); return $self->_canSeek(0); } - + # Is this pass-through? if ($transcoder->{'command'} eq '-') { return $self->_canSeek(1); # nice simple case } - + # no, then could we get a seeking transcoder? if (Slim::Player::TranscodingHelper::getConvertCommand2( $self, @@ -847,7 +842,7 @@ sub canDoSeek { { return $self->_canSeek(2); } - + # no, then did the transcoder accept stdin? if ($transcoder->{'streamMode'} eq 'I') { return $self->_canSeek(1); @@ -855,12 +850,12 @@ sub canDoSeek { $self->_canSeekError([ 'SEEK_ERROR_TRANSCODED' ]); return $self->_canSeek(0); } - + } else { $self->_canSeekError([$handler->can('canSeekError') ? $handler->canSeekError( $self->master(), $self ) : ('SEEK_ERROR_REMOTE')]); - + # Note: this is intended to fall through to the below code } } @@ -872,24 +867,24 @@ sub canDoSeek { { return $self->_canSeek(2); } - + if (!$self->_canSeekError()) { $self->_canSeekError([ 'SEEK_ERROR_REMOTE' ]); } - + return $self->_canSeek(0); } } # This is a prototype, that just falls back to protocol-handler providers (pull) for now. -# It is planned to move the actual metadata maintenance into this module where the +# It is planned to move the actual metadata maintenance into this module where the # protocol-handlers will push the data. sub metadata { my ($self) = @_; - + my $handler; - + if (($handler = $self->_currentTrackHandler()) && $handler->can('songMetadata') || ($handler = $self->handler()) && $handler->can('songMetadata') ) { @@ -900,22 +895,22 @@ sub metadata { { return $handler->songMetadata($self->master, $self->currentTrackHandler()->url, 0); } - + return undef; } sub icon { my $self = shift; my $client = $self->master(); - + my $icon = Slim::Player::ProtocolHandlers->iconForURL($self->currentTrack()->url, $client); - + $icon ||= Slim::Player::ProtocolHandlers->iconForURL($self->track()->url, $client); - + if (!$icon && $self->currentTrack()->isa('Slim::Schema::Track')) { $icon = '/music/' . $self->currentTrack()->coverid . '/cover.jpg' } - + return $icon; } diff --git a/Slim/Player/SongStreamController.pm b/Slim/Player/SongStreamController.pm index 8e513b40705..61face4d506 100644 --- a/Slim/Player/SongStreamController.pm +++ b/Slim/Player/SongStreamController.pm @@ -1,8 +1,7 @@ package Slim::Player::SongStreamController; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Source.pm b/Slim/Player/Source.pm index 82bff454ce5..57fb41a6e18 100644 --- a/Slim/Player/Source.pm +++ b/Slim/Player/Source.pm @@ -1,8 +1,7 @@ package Slim::Player::Source; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/SqueezePlay.pm b/Slim/Player/SqueezePlay.pm index 4bb6b91a94b..5c33127d0e0 100644 --- a/Slim/Player/SqueezePlay.pm +++ b/Slim/Player/SqueezePlay.pm @@ -1,6 +1,6 @@ package Slim::Player::SqueezePlay; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -41,6 +41,7 @@ BEGIN { hasPolarityInversion spDirectHandlers proxyAddress + _canHTTPS )); } @@ -58,6 +59,7 @@ sub new { firmware => 0, canDecodeRhapsody => 0, canDecodeRtmp => 0, + _canHTTPS => 0, hasDigitalOut => 0, hasPreAmp => 0, hasDisableDac => 0, @@ -86,6 +88,7 @@ my %CapabilitiesMap = ( SyncgroupID => undef, Spdirect => 'spDirectHandlers', Proxy => 'proxyAddress', + CanHTTPS => '_canHTTPS', # deprecated model => '_model', @@ -96,6 +99,10 @@ sub model { return shift->_model; } +sub canHTTPS { + return shift->_canHTTPS; +} + # This will return the full version + revision, i.e. 7.5.0 r8265 sub revision { return shift->firmware; @@ -232,7 +239,7 @@ sub pcm_sample_rates { sub fade_volume { my ($client, $fade, $callback, $callbackargs) = @_; - if (abs($fade) > 1 ) { + if (abs($fade) > 1 || $client->model !~ /baby|fab4/) { # for long fades do standard behavior so that sleep/alarm work $client->SUPER::fade_volume($fade, $callback, $callbackargs); } else { diff --git a/Slim/Player/SqueezeSlave.pm b/Slim/Player/SqueezeSlave.pm index a2206083357..a4318c3d77e 100644 --- a/Slim/Player/SqueezeSlave.pm +++ b/Slim/Player/SqueezeSlave.pm @@ -1,8 +1,7 @@ package Slim::Player::SqueezeSlave; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Squeezebox.pm b/Slim/Player/Squeezebox.pm index 915600f5648..286c80c9f7d 100644 --- a/Slim/Player/Squeezebox.pm +++ b/Slim/Player/Squeezebox.pm @@ -1,6 +1,6 @@ package Slim::Player::Squeezebox; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -676,6 +676,15 @@ sub stream_s { $pcmchannels = '?'; $outputThreshold = 20; + } elsif ($format eq 'ops') { + + $formatbyte = 'u'; + $pcmsamplesize = '?'; + $pcmsamplerate = '?'; + $pcmendian = '?'; + $pcmchannels = '?'; + $outputThreshold = 20; + } elsif ($format eq 'alc') { $formatbyte = 'l'; @@ -710,7 +719,7 @@ sub stream_s { } elsif ($format eq 'dff' || $format eq 'dsf') { $formatbyte = 'd'; - $pcmsamplesize = '?'; + $pcmsamplesize = $format eq 'dsf' ? 0 : 1; $pcmsamplerate = '?'; $pcmendian = '?'; $pcmchannels = '?'; @@ -918,7 +927,9 @@ sub stream_s { if ( $prefs->client($master)->get('transitionSmart') && - ( Slim::Player::ReplayGain->trackAlbumMatch( $master, -1 ) + ( Slim::Player::Playlist::count($master) < 2 + || + Slim::Player::ReplayGain->trackAlbumMatch( $master, -1 ) || Slim::Player::ReplayGain->trackAlbumMatch( $master, 1 ) ) @@ -962,14 +973,22 @@ sub stream_s { # by a player preference. my $transitionSampleRestriction = $prefs->client($master)->get('transitionSampleRestriction') || 0; - if (!Slim::Player::ReplayGain->trackSampleRateMatch($master, -1) && $transitionSampleRestriction) { - main::INFOLOG && $log->info('Overriding crossfade due to differing sample rates'); + if ($transitionSampleRestriction && ($transitionType == 1 || $transitionType == 5) && !Slim::Player::ReplayGain->trackSampleRateMatch($master, -1)) { + main::INFOLOG && $log->info('Overriding crossfade due to differing sample rates or single track'); $transitionType = 0; } elsif ($transitionSampleRestriction) { main::INFOLOG && $log->info('Crossfade sample rate restriction enabled but not needed for this transition'); } - - } + + # this is crossfade, so only apply fade-in if we are already playing (exclude start, resume, reposition actions) + if (!$master->isPlaying(1)) { + if ($transitionType == 2) { + $transitionType = 0; + } elsif ($transitionType == 4) { + $transitionType = 3; + } + } + } if ($transitionDuration > $client->maxTransitionDuration()) { $transitionDuration = $client->maxTransitionDuration(); diff --git a/Slim/Player/Squeezebox1.pm b/Slim/Player/Squeezebox1.pm index 3540703f1df..17709590ba9 100644 --- a/Slim/Player/Squeezebox1.pm +++ b/Slim/Player/Squeezebox1.pm @@ -1,8 +1,7 @@ package Slim::Player::Squeezebox1; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Squeezebox2.pm b/Slim/Player/Squeezebox2.pm index edfdd74c4aa..a7698f4afb6 100644 --- a/Slim/Player/Squeezebox2.pm +++ b/Slim/Player/Squeezebox2.pm @@ -1,8 +1,7 @@ package Slim::Player::Squeezebox2; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -579,7 +578,7 @@ sub directHeaders { # Bitrate may have been set in Scanner by reading the mp3 stream if ( !$bitrate ) { - $bitrate = Slim::Music::Info::getBitrate( $url ); + $bitrate = Slim::Music::Info::getBitrate( $url ) || 0; } # WMA handles duration based on metadata diff --git a/Slim/Player/StreamingController.pm b/Slim/Player/StreamingController.pm index 845921c9d8f..92217f78595 100644 --- a/Slim/Player/StreamingController.pm +++ b/Slim/Player/StreamingController.pm @@ -1,8 +1,7 @@ package Slim::Player::StreamingController; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -333,6 +332,14 @@ sub _Buffering {_setPlayingState($_[0], BUFFERING);} sub _Playing { my ($self) = @_; + # need to fade-in here and not in _Stream due to potential buffering delay + if ($self->{'fadeActive'}) { + foreach my $player (@{$self->{'players'}}) { + $player->fade_volume($prefs->client($self->master)->get('fadeInDuration')); + } + } + $self->{'fadeActive'} = undef; + # bug 10681 - don't actually change the state if we are rebuffering # as there can be a race condition between output buffer underrun and # track-start events especially, but not exclusively when synced. @@ -1239,23 +1246,33 @@ sub _Stream { # play -> Buffering, Streaming my $fadeIn = $self->{'fadeIn'} || 0; $paused ||= ($fadeIn > 0); - my $setVolume = $self->{'playingState'} == STOPPED; - my $masterVol = abs($prefs->client($self->master())->get("volume") || 0); + # Bug 18165: master's volume might not be the right one to use + my $masterVol; + foreach my $player (@{$self->{'players'}}) { + next unless $prefs->client($player)->get('syncVolume'); + $masterVol = abs($prefs->client($player)->get("volume") || 0); + last; + } my $startedPlayers = 0; my $reportsTrackStart = 0; - + # bug 10438 $self->resetFrameData(); + # fade-in has precedence over crossfade only player is stopped + $self->{'fadeActive'} = $fadeIn < 1 && $self->{'playingState'} == STOPPED && $prefs->client($self->master)->get('fadeInDuration'); + foreach my $player (@{$self->{'players'}}) { - if ($setVolume) { + if ($self->{'playingState'} == STOPPED) { # Bug 10310: Make sure volume is synced if necessary my $vol = ($prefs->client($player)->get('syncVolume')) ? $masterVol : abs($prefs->client($player)->get("volume") || 0); $player->volume($vol); - } + # can't fade_volume here as we might not be playing yet so ramp-up would be lost + $player->volume(0,1) if $self->{'fadeActive'}; + } my $myFadeIn = $fadeIn; if ($fadeIn > $player->maxTransitionDuration()) { @@ -1556,6 +1573,7 @@ sub _JumpOrResume { # resume -> Streaming, Playing if (defined $self->{'resumeTime'}) { $self->{'fadeIn'} = FADEVOLUME; + _JumpToTime($self, $event, {newtime => $self->{'resumeTime'}, restartIfNoSeek => 1}); $self->{'resumeTime'} = undef; @@ -1571,7 +1589,9 @@ sub _Resume { # resume -> Playing my $song = playingSong($self); my $pausedAt = ($self->{'resumeTime'} || 0) - ($song ? ($song->startOffset() || 0) : 0); my $startAtBase = Time::HiRes::time() + ($prefs->get('syncStartDelay') || 200) / 1000; - + + $self->{fadeIn} ||= $prefs->client($self->master)->get('fadeInDuration'); + _setPlayingState($self, PLAYING); foreach my $player (@{$self->{'players'}}) { # set volume to 0 to make sure fade works properly diff --git a/Slim/Player/Sync.pm b/Slim/Player/Sync.pm index b22c014247a..63ca787f2e0 100644 --- a/Slim/Player/Sync.pm +++ b/Slim/Player/Sync.pm @@ -1,8 +1,7 @@ package Slim::Player::Sync; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/TranscodingHelper.pm b/Slim/Player/TranscodingHelper.pm index 2cf68ac57b2..8456ce2801e 100644 --- a/Slim/Player/TranscodingHelper.pm +++ b/Slim/Player/TranscodingHelper.pm @@ -1,8 +1,7 @@ package Slim::Player::TranscodingHelper; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Player/Transporter.pm b/Slim/Player/Transporter.pm index 7fb6c03cecd..bb956a9b8df 100644 --- a/Slim/Player/Transporter.pm +++ b/Slim/Player/Transporter.pm @@ -1,8 +1,7 @@ package Slim::Player::Transporter; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/ACLFiletest/Plugin.pm b/Slim/Plugin/ACLFiletest/Plugin.pm index 5d7d851a315..b3be1e30288 100644 --- a/Slim/Plugin/ACLFiletest/Plugin.pm +++ b/Slim/Plugin/ACLFiletest/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::ACLFiletest::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/Amazon/Plugin.pm b/Slim/Plugin/Amazon/Plugin.pm index a088bb25942..8ae4a9d0fb2 100644 --- a/Slim/Plugin/Amazon/Plugin.pm +++ b/Slim/Plugin/Amazon/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Amazon::Plugin; -# $Id$ use strict; use base qw(Slim::Plugin::OPMLBased); diff --git a/Slim/Plugin/AppGallery/Plugin.pm b/Slim/Plugin/AppGallery/Plugin.pm index 4644abda92a..126bc54e143 100644 --- a/Slim/Plugin/AppGallery/Plugin.pm +++ b/Slim/Plugin/AppGallery/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::AppGallery::Plugin; -# $Id$ use strict; use base qw(Slim::Plugin::OPMLBased); diff --git a/Slim/Plugin/AudioScrobbler/HTML/EN/plugins/AudioScrobbler/settings/basic.html b/Slim/Plugin/AudioScrobbler/HTML/EN/plugins/AudioScrobbler/settings/basic.html index 246e5f1a513..f2931128dde 100644 --- a/Slim/Plugin/AudioScrobbler/HTML/EN/plugins/AudioScrobbler/settings/basic.html +++ b/Slim/Plugin/AudioScrobbler/HTML/EN/plugins/AudioScrobbler/settings/basic.html @@ -40,4 +40,20 @@ [% END %] + [% WRAPPER setting title="PLUGIN_AUDIOSCROBBLER_IGNORE_TITLES" desc="PLUGIN_AUDIOSCROBBLER_IGNORE_TITLES_DESC" %] + + [% END %] + + [% WRAPPER setting title="PLUGIN_AUDIOSCROBBLER_IGNORE_ARTISTS" desc="PLUGIN_AUDIOSCROBBLER_IGNORE_ARTISTS_DESC" %] + + [% END %] + + [% WRAPPER setting title="PLUGIN_AUDIOSCROBBLER_IGNORE_ALBUMS" desc="PLUGIN_AUDIOSCROBBLER_IGNORE_ALBUMS_DESC" %] + + [% END %] + + [% WRAPPER setting title="PLUGIN_AUDIOSCROBBLER_IGNORE_GENRES" desc="PLUGIN_AUDIOSCROBBLER_IGNORE_GENRES_DESC" %] + + [% END %] + [% PROCESS settings/footer.html %] diff --git a/Slim/Plugin/AudioScrobbler/PlayerSettings.pm b/Slim/Plugin/AudioScrobbler/PlayerSettings.pm index 28af2c31e51..4208d60ca42 100644 --- a/Slim/Plugin/AudioScrobbler/PlayerSettings.pm +++ b/Slim/Plugin/AudioScrobbler/PlayerSettings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::AudioScrobbler::PlayerSettings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/AudioScrobbler/Plugin.pm b/Slim/Plugin/AudioScrobbler/Plugin.pm index ca7f9a09390..1dbfe4f6def 100644 --- a/Slim/Plugin/AudioScrobbler/Plugin.pm +++ b/Slim/Plugin/AudioScrobbler/Plugin.pm @@ -1,11 +1,10 @@ package Slim::Plugin::AudioScrobbler::Plugin; -# $Id$ # This plugin handles submission of tracks to Last.fm's # Audioscrobbler service. -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -77,6 +76,10 @@ sub initPlugin { enable_scrobbling => 1, include_radio => 0, account => 0, + ignoreTitles => '', + ignoreGenres => '', + ignoreArtists => '', + ignoreAlbums => '', }); # Subscribe to new song events @@ -419,12 +422,13 @@ sub newsongCallback { my $track = Slim::Schema->objectForUrl( { url => $url } ); my $duration = $track->secs; + my $meta; if ( $track->remote ) { my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url); if ( $handler && $handler->can('getMetadataFor') ) { # this plugin provides track metadata, i.e. Pandora, Rhapsody - my $meta = $handler->getMetadataFor( $client, $url, 'forceCurrent' ); + $meta = $handler->getMetadataFor( $client, $url, 'forceCurrent' ); if ( $meta && $meta->{duration} ) { $duration = $meta->{duration}; } @@ -478,11 +482,8 @@ sub newsongCallback { my $title = $track->title; if ( $track->remote ) { - my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url); - if ( $handler && $handler->can('getMetadataFor') ) { - # this plugin provides track metadata, i.e. Pandora, Rhapsody - my $meta = $handler->getMetadataFor( $client, $url, 'forceCurrent' ); - $title = $meta->{title}; + if ( $meta ) { + $title = $meta->{title}; # Handler must return at least artist and title unless ( $meta->{artist} && $meta->{title} ) { @@ -499,6 +500,22 @@ sub newsongCallback { } } + $meta ||= {}; + + my @ignoreTitles = split(/\s*,\s*/, $prefs->get('ignoreTitles')); + my @ignoreAlbums = split(/\s*,\s*/, $prefs->get('ignoreAlbums')); + my @ignoreArtists = split(/\s*,\s*/, $prefs->get('ignoreArtists')); + my @ignoreGenres = split(/\s*,\s*/, $prefs->get('ignoreGenres')); + + if ( (scalar @ignoreGenres && $track->genre && grep { $track->genre->name =~ /\Q$_\E/i } @ignoreGenres ) + || (scalar @ignoreTitles && grep { $title =~ /\Q$_\E/i } @ignoreTitles) + || (scalar @ignoreArtists && grep { ($track->artistName || $meta->{artist} || '') =~ /\Q$_\E/i } @ignoreArtists) + || (scalar @ignoreAlbums && grep { ($track->albumname || $meta->{album} || '') =~ /\Q$_\E/i } @ignoreAlbums) + ) { + main::DEBUGLOG && $log->debug( sprintf("Ignoring %s, it's failing one of the ignored items tests: %s, %s, %s", $title, $track->artistName || $meta->{artist}, $track->albumname || $meta->{album}, ($track->genre ? $track->genre->name : '')) ); + return; + } + # We check again at half the song's length or 240 seconds, whichever comes first my $checktime; if ( $duration ) { diff --git a/Slim/Plugin/AudioScrobbler/Settings.pm b/Slim/Plugin/AudioScrobbler/Settings.pm index 8be7e05687a..9986629b0ad 100644 --- a/Slim/Plugin/AudioScrobbler/Settings.pm +++ b/Slim/Plugin/AudioScrobbler/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::AudioScrobbler::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -25,7 +25,7 @@ sub page { } sub prefs { - return ( $prefs, qw(accounts enable_scrobbling include_radio) ); + return ( $prefs, qw(accounts enable_scrobbling include_radio ignoreTitles ignoreGenres ignoreArtists ignoreAlbums) ); } sub handler { diff --git a/Slim/Plugin/AudioScrobbler/strings.txt b/Slim/Plugin/AudioScrobbler/strings.txt index 1c15dc6c1bd..f0a00d6bef5 100644 --- a/Slim/Plugin/AudioScrobbler/strings.txt +++ b/Slim/Plugin/AudioScrobbler/strings.txt @@ -37,7 +37,7 @@ SETUP_PLUGIN_AUDIOSCROBBLER_ACCOUNTS_DESC FI Anna Last.fm-tilin käyttäjätunnus ja salasana. Voit antaa useita tilejä, jos haluat lähettää tiedot kuunnelluista kappaleista eri soittimista eri tileihin. FR Spécifiez votre nom d'utilisateur et votre mot de passe Last.fm. Vous pouvez spécifier plusieurs comptes si vous souhaitez envoyer des morceaux de platines différentes vers des comptes différents. IT Inserire il nome utente e la password di Last.fm. Se si desidera inviare brani da lettori diversi a più account, è possibile creare diversi account. - NL Voer je gebruikersnaam en wachtwoord voor Last.fm in. Als je nummers van verschillende muzieksystemen bij verschillende accounts wilt indienen, kun je meerdere accounts invoeren. + NL Voer je gebruikersnaam en wachtwoord voor Last.fm in. Als je nummers van verschillende muziekspelers bij verschillende accounts wilt indienen, kun je meerdere accounts invoeren. NO Angi brukernavn og passord for Last.fm. Du kan angi flere kontoer hvis du vil sende spor fra ulike spillere til ulike kontoer. PL Wprowadź nazwę użytkownika i hasło w usłudze Last.fm. W celu przesłania informacji o utworach z różnych odtwarzaczy na różne konta można wprowadzić dane z kilku kont. RU Введите имя пользователя и пароль Last.fm. Можно указать несколько учетных записей, чтобы передавать дорожки с разных плееров в разные учетные записи. @@ -133,6 +133,38 @@ SETUP_PLUGIN_AUDIOSCROBBLER_ENABLE_SCROBBLE_NO RU Нет, не сообщать о воспроизводимых дорожках на Last.fm. SV Nej, rapportera inte spelade spår till Last.fm. +PLUGIN_AUDIOSCROBBLER_IGNORE_TITLES + DE Titel ignorieren + EN Ignore Titles + +PLUGIN_AUDIOSCROBBLER_IGNORE_TITLES_DESC + DE Geben Sie eine Komma-separierte Liste von Titeln ein. Abgespielte Lieder, die einen dieser Begriffe enthalten, werden nicht an Last.fm gemeldet (gescrobbelt). + EN Define a comma separated list of titles or names. Tracks which match one of these strings will not be scrobbled. + +PLUGIN_AUDIOSCROBBLER_IGNORE_ARTISTS + DE Interpreten ignorieren + EN Ignore Artists + +PLUGIN_AUDIOSCROBBLER_IGNORE_ARTISTS_DESC + DE Geben Sie eine Komma-separierte Liste von Interpreten ein. Abgespielte Lieder, die einen dieser Begriffe enthalten, werden nicht an Last.fm gemeldet (gescrobbelt). + EN Define a comma separated list list of artist names. Tracks which match one of these strings will not be scrobbled. + +PLUGIN_AUDIOSCROBBLER_IGNORE_ALBUMS + DE Albentitel ignorieren + EN Ignore Album Titles + +PLUGIN_AUDIOSCROBBLER_IGNORE_ALBUMS_DESC + DE Geben Sie eine Komma-separierte Liste von Albumtiteln ein. Abgespielte Lieder, die einen dieser Begriffe enthalten, werden nicht an Last.fm gemeldet (gescrobbelt). + EN Define a comma separated list of album names. Tracks which match one of these strings will not be scrobbled. + +PLUGIN_AUDIOSCROBBLER_IGNORE_GENRES + DE Stilrichtungen ignorieren + EN Ignore Genres + +PLUGIN_AUDIOSCROBBLER_IGNORE_GENRES_DESC + DE Geben Sie eine Komma-separierte Liste von Stilrichtungen ein. Abgespielte Lieder, die einen dieser Begriffe enthalten, werden nicht an Last.fm gemeldet (gescrobbelt). + EN Define a comma separated list list of genres. Tracks which match one of these strings will not be scrobbled. + PLUGIN_AUDIOSCROBBLER_BANNED CS Tato verze Logitech Media Server byla zakázána prostřednictvím Last.fm, aktualizujte laskavě na novější verzi. DA Denne version af Logitech Media Server ser ud til at være udelukket af Last.fm. Du bør opgradere til en nyere version. @@ -202,7 +234,7 @@ SETUP_PLUGIN_AUDIOSCROBBLER_PLAYER FI Last.fm Audioscrobbler -soittimen asetukset FR Paramètres de Last.fm Audioscrobbler. IT Impostazioni del lettore Audioscrobbler Last.fm - NL Muzieksysteeminstellingen voor Last.fm Audioscrobbler + NL Muziekspelerinstellingen voor Last.fm Audioscrobbler NO Spillerinnstillinger for Last.fm Audioscrobbler PL Ustawienia odtwarzacza w usłudze Last.fm Audioscrobbler RU Настройки скробблера Last.fm @@ -217,7 +249,7 @@ SETUP_PLUGIN_AUDIOSCROBBLER_PLAYER_DESC FI Jos haluat ottaa Audioscrobblerin käyttöön tässä soittimessa, valitse käytettävä tili. FR Pour activer Audioscrobbler pour cette platine, sélectionnez un compte à utiliser. IT Per abilitare Audioscrobbler per questo lettore, selezionare l'account da utilizzare. - NL Selecteer een te gebruiken account om Audioscrobbler voor dit muzieksysteem in te schakelen. + NL Selecteer een te gebruiken account om Audioscrobbler voor deze muziekspeler in te schakelen. NO Hvis du vil aktivere Audioscrobbler for denne spilleren, må du velge en konto. PL Aby włączyć funkcję Audioscrobbler dla tego odtwarzacza, wybierz konto do użycia. RU Чтобы активировать Audioscrobbler на этом плеере, выберите учетную запись. @@ -232,7 +264,7 @@ SETUP_PLUGIN_AUDIOSCROBBLER_PLAYER_DISABLED FI Ei käytössä. Älä lähetä tietoja tällä soittimella soitetuista kappaleista. FR Désactivé, ne pas envoyer les morceaux lus sur cette platine. IT Disattiva. Non inviare i brani riprodotti con questo lettore. - NL Uitgeschakeld; nummers gespeeld op dit muzieksysteem niet indienen. + NL Uitgeschakeld; nummers gespeeld op deze muziekspeler niet indienen. NO Deaktivert, ikke send inn spor som spilles på denne spilleren. PL Wyłączone, nie wysyłaj informacji o odtwarzanych utworach. RU Отключено, не передавать дорожки, воспроизводимые на этом плеере. @@ -247,7 +279,7 @@ SETUP_PLUGIN_AUDIOSCROBBLER_WARNING_SCROBBLE_DISABLED FI Varoitus: Kaikkien kappaleiden lähettäminen on yleisesti poistettu käytöstä. Jos haluat lähettää tiedot tällä soittimella soitetuista kappaleista, ota Lähetä kaikki kappaleet -asetus käyttöön kohdassa Lisäasetukset -> Last.fm Audioscrobbler. FR Avertissement: l'envoi de tous les morceaux est désactivé de manière globale. Pour envoyer les informations des morceaux pour cette platine, activez l'option pour envoyer tous les morceaux dans Avancé -> Last.fm Audioscrobbler. IT Avviso: l'invio di tutti i brani è stato disattivato a livello globale. Per inviare i dati relativi ai brani del lettore corrente, attivare l'impostazione Invia tutti i brani, accessibile dalla scheda Avanzate -> Audioscrobbler Last.fm. - NL Waarschuwing: Alle nummers indienen is globaal uitgeschakeld. Als je nummergegevens voor dit muzieksysteem wilt indienen, schakel je Alle nummers indienen in via Geavanceerd -> Last.fm Audioscrobbler. + NL Waarschuwing: Alle nummers indienen is globaal uitgeschakeld. Als je nummergegevens voor deze muziekspeler wilt indienen, schakel je Alle nummers indienen in via Geavanceerd -> Last.fm Audioscrobbler. NO Advarsel: Innsending av spor er deaktivert i hele systemet. Hvis du vil sende informasjon om avspilte spor fra denne spilleren, må du aktivere innstillingen Send alle spor under Avansert -> Last.fm Audioscrobbler. PL Ostrzeżenie: wysyłanie informacji o wszystkich utworach jest globalnie wyłączone. Aby przesyłać informacje do tego odtwarzacza, włącz ustawienie przesyłania informacji o wszystkich utworach, wybierając opcję Zaawansowane -> Last.fm Audioscrobbler. RU Предупреждение. Параметр "Передавать все дорожки" глобально отключен. Чтобы передавать информацию о дорожках для этого плеера, перейдите на "Дополнительно" -> "Last.fm Аудиоскробблер" и включите "Передавать все дорожки". @@ -262,7 +294,7 @@ SETUP_PLUGIN_AUDIOSCROBBLER_PLAYER_NOTE FI Huomaa: Audioscrobbler täytyy myös ottaa käyttöön kussakin soittimessa. Napsauta Soitin-välilehdellä ja valitse Last.fm Audioscrobbler jokaiselle soittimelle, josta haluat lähettää tiedot kuunnelluista kappaleista Last.fm:ään. FR Remarque: vous devez également activer Audioscrobbler pour chaque platine. Cliquez sur l'onglet Platine et sélectionner Last.fm Audioscrobbler pour chaque platine. IT Nota: è inoltre necessario attivare Audioscrobbler per ogni lettore. Fare clic sulla scheda Lettore e selezionare la voce Audioscrobbler Last.fm per ogni lettore che si desidera monitorare. - NL Opmerking: Je moet ook Autoscrobbler voor elk muzieksysteem inschakelen. Klik op de tab Muzieksysteem en selecteer het item Last.fm Audioscrobbler voor elk systeem dat je wilt scrobbelen. + NL Opmerking: Je moet ook Autoscrobbler voor elke muziekspeler inschakelen. Klik op de tab Muziekspeler en selecteer het item Last.fm Audioscrobbler voor elk systeem dat je wilt scrobbelen. NO Merk: Du må også aktivere Audioscrobbler for hver spiller. Klikk på kategorien Spiller, og velg Last.fm Audioscrobbler-elementet for hver spiller du vil bruke med Audioscrobbler. PL Uwaga: Funkcję Audioscrobbler należy włączyć dla każdego odtwarzacza. Kliknij kartę Odtwarzacz, a następnie wybierz pozycję Last.fm Audioscrobbler dla każdego odtwarzacza, z którego chcesz wysłać informacje. RU Примечание. Нужно также включить скробблер для каждого плеера. Щелкните вкладку "Плеер" и выберите элемент "Скробблер Last.fm" для каждого плеера, который необходимо скробблить. diff --git a/Slim/Plugin/Base.pm b/Slim/Plugin/Base.pm index 58aaf9940a1..a15330f0f87 100644 --- a/Slim/Plugin/Base.pm +++ b/Slim/Plugin/Base.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Base; -# $Id$ # Base class for plugins. Implement some basics. diff --git a/Slim/Plugin/CLI/Plugin.pm b/Slim/Plugin/CLI/Plugin.pm index 0dc1118ff17..816ddc51d4e 100644 --- a/Slim/Plugin/CLI/Plugin.pm +++ b/Slim/Plugin/CLI/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::CLI::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -243,8 +243,14 @@ sub cli_socket_accept { my $tmpaddr = inet_ntoa($client_socket->peeraddr); # Check allowed hosts - - if (!($prefsServer->get('filterHosts')) || (Slim::Utils::Network::isAllowedHost($tmpaddr))) { + if ( !Slim::Utils::Network::ip_is_host($tmpaddr) + && $prefsServer->get('protectSettings') && !$prefsServer->get('authorize') + && ( Slim::Utils::Network::ip_is_gateway($tmpaddr) || Slim::Utils::Network::ip_on_different_network($tmpaddr) ) + ) { + $log->error("Access to CLI is restricted to the local network or localhost: $tmpaddr"); + $client_socket->close; + } + elsif (!($prefsServer->get('filterHosts')) || (Slim::Utils::Network::isAllowedHost($tmpaddr))) { Slim::Networking::Select::addRead($client_socket, \&client_socket_read); Slim::Networking::Select::addError($client_socket, \&client_socket_close); diff --git a/Slim/Plugin/CLI/Settings.pm b/Slim/Plugin/CLI/Settings.pm index ea18fbaa648..df54373f193 100644 --- a/Slim/Plugin/CLI/Settings.pm +++ b/Slim/Plugin/CLI/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::CLI::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/CLI/strings.txt b/Slim/Plugin/CLI/strings.txt index ea328cff5dc..c7ab4d09331 100644 --- a/Slim/Plugin/CLI/strings.txt +++ b/Slim/Plugin/CLI/strings.txt @@ -1,7 +1,5 @@ # String file for CLI plugin -# $Id: strings.txt 10924 2006-12-10 10:07:21Z mherger $ - PLUGIN_CLI CS Command Line Interface (Povelové rozhraní) (CLI) DA Kommandolinjegrænseflade @@ -12,7 +10,7 @@ PLUGIN_CLI FR Interface de ligne de commande (CLI) HE ממשק שורת פקודה (CLI) IT Interfaccia della riga di comando - NL Opdrachtregelinterface + NL Opdrachtregelinterface (CLI) NO Kommandolinjegrensesnitt PL Interfejs wiersza polecenia (CLI) RU Интерфейс командной строки (CLI) @@ -28,7 +26,7 @@ PLUGIN_CLI_DESC FR Le plugin d'interface de ligne de commande permet de contrôler le logiciel Squeezebox et les platines à distance par le biais d'une connexion TCP/IP, par exemple par un système d'automatisation tiers, tel qu'AMX ou Crestron. HE יישום ה-Plugin‏ 'ממשק שורת פקודה' מאפשר שליטה מרחוק ב-Logitech Media Server וב-Squeezebox באמצעות קישור TCP/IP, לדוגמה, על-ידי מערכת אוטומציה של צד שלישי כגון AMX או Crestron. IT Il plugin dell'interfaccia della riga di comando consente di controllare Squeezebox e i lettori in remoto su una connessione TCP/IP mediante, ad esempio, un sistema di automatizzazione di terze parti, quale AMX o Crestron. - NL Dankzij de plug-in van de opdrachtregelinterface is het mogelijk om de Squeezebox-software en systemen op afstand te bedienen via een TCP/IP-verbinding, bijvoorbeeld door een automatiseringssysteem van een derde partij zoals AMX of Creston. + NL Met de opdrachtregelinterface-plugin is het mogelijk om de Squeezebox-software en systemen op afstand te bedienen via een TCP/IP-verbinding, bijvoorbeeld door een automatiseringssysteem van een derde partij zoals AMX of Creston. NO Med kommandolinjegrensesnittet kan Squeezebox-programvaren og spillerne kontrolleres eksternt via en TCP/IP-tilkopling, for eksempel av et tredjeparts automatiseringssystem som AMX eller Crestron. PL Dodatek interfejsu wiersza polecenia umożliwia obsługę oprogramowania Squeezebox i odtwarzaczy zdalnie przez połączenie TCP/IP, na przykład przez automatyczny system innej firmy, taki jak AMX lub Crestron. RU Плагин "Интерфейс командной строки" позволяет управлять Squeezebox и плеерами удаленно по подключению TCP/IP, например, с помощью сторонней автоматической системы, такой как AMX или Crestron. @@ -64,7 +62,7 @@ SETUP_CLIPORT_DESC HE באפשרותך לשנות את מספר היציאה המשמשת את ממשק שורת הפקודה לשליטה בנגן. IT È possibile modificare il numero della porta usata dall'interfaccia a riga di comando per controllare il lettore. JA プレーヤーをコントロールする、コマンドライン インターフェースに使われるポートナンバーを変更することができます。 - NL Je kunt het poortnummer veranderen dat door een opdrachtregelinterface gebruikt wordt om het muzieksysteem te bedienen. + NL Je kunt het poortnummer wijzigen dat door een opdrachtregelinterface gebruikt wordt om de muziekspeler te bedienen. NO Du kan endre portnummeret som brukes for å kontrollere spilleren via et kommandolinjegrensesnitt. PL Możliwa jest zmiana numeru portu używanego przez interfejs wiersza polecenia w celu sterowania odtwarzaczem. PT Pode mudar o número da porta para ligação da interface de linha de comando do player. diff --git a/Slim/Plugin/Classical/Plugin.pm b/Slim/Plugin/Classical/Plugin.pm index df6447f4ef7..f68e81e2ba8 100644 --- a/Slim/Plugin/Classical/Plugin.pm +++ b/Slim/Plugin/Classical/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Classical::Plugin; -# $Id$ use strict; use base qw(Slim::Plugin::OPMLBased); @@ -16,7 +15,7 @@ sub initPlugin { ); Slim::Player::ProtocolHandlers->registerIconHandler( - qr|squeezenetwork\.com.*/api/classical/|, + qr|mysqueezebox\.com.*/api/classical/|, sub { $class->_pluginDataFor('icon') } ); diff --git a/Slim/Plugin/Classical/ProtocolHandler.pm b/Slim/Plugin/Classical/ProtocolHandler.pm index 4c43eb0201e..37f55fcd99d 100644 --- a/Slim/Plugin/Classical/ProtocolHandler.pm +++ b/Slim/Plugin/Classical/ProtocolHandler.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Classical::ProtocolHandler; -# $Id$ # Handler for classical:// URLs diff --git a/Slim/Plugin/DateTime/Plugin.pm b/Slim/Plugin/DateTime/Plugin.pm index a4cb355b60f..1f37d70e9ac 100644 --- a/Slim/Plugin/DateTime/Plugin.pm +++ b/Slim/Plugin/DateTime/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::DateTime::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/DateTime/Settings.pm b/Slim/Plugin/DateTime/Settings.pm index 6608ae23f30..42aecd7146c 100644 --- a/Slim/Plugin/DateTime/Settings.pm +++ b/Slim/Plugin/DateTime/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::DateTime::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/DateTime/strings.txt b/Slim/Plugin/DateTime/strings.txt index 052924d901f..024da90a6c7 100644 --- a/Slim/Plugin/DateTime/strings.txt +++ b/Slim/Plugin/DateTime/strings.txt @@ -1,7 +1,5 @@ # String file for DateTime plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_SCREENSAVER_DATETIME CS Datum a čas DA Dato og klokkeslæt @@ -44,7 +42,7 @@ SETUP_GROUP_DATETIME_DESC FR Ces réglages ajustent les paramètres de l'Ecran de veille Date/Heure. HE הגדרות אלה שולטות בהתנהגות של שומר המסך של תאריך ושעה. IT Queste impostazioni consentono di controllare il funzionamento dello screen saver con data e ora - NL Deze instellingen bepalen het gedrag van de datum en tijd-schermbeveiliger + NL Stel hier het formaat van de datum en tijd-schermbeveiliger in. NO Disse innstillingene kontrollerer virkemåten til skjermspareren som viser dato og klokkeslett PL Te ustawienia umożliwiają dostosowanie sposobu działania wygaszacza z datą i godziną RU Эти настройки управляют экранной заставкой "Дата и время" @@ -60,7 +58,7 @@ SETUP_GROUP_DATETIME_DEFAULTTIME FR Valeur par défaut du serveur HE ברירת המחדל של Logitech Media Server IT Impostazioni predefinite di server - NL Standaardinstellingen van Logitech Media Server + NL Standaardinstelling server NO Serverstandard PL Domyślne ustawienie serwera RU Серверное значение по умолчанию @@ -76,7 +74,7 @@ SETUP_GROUP_DATETIME_DEFAULTDATE FR Défaut serveur HE ברירת המחדל של Logitech Media Server IT Impostazioni predefinite di server - NL Standaardinstellingen van Logitech Media Server + NL Standaardinstelling server NO Server-standard PL Domyślne ustawienie serwera RU Серверное значение по умолчанию diff --git a/Slim/Plugin/Deezer/Plugin.pm b/Slim/Plugin/Deezer/Plugin.pm index 9f84598a7d2..792d45fbcf8 100644 --- a/Slim/Plugin/Deezer/Plugin.pm +++ b/Slim/Plugin/Deezer/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::Deezer::Plugin; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/Deezer/ProtocolHandler.pm b/Slim/Plugin/Deezer/ProtocolHandler.pm index e95e98f4791..9e1de668969 100644 --- a/Slim/Plugin/Deezer/ProtocolHandler.pm +++ b/Slim/Plugin/Deezer/ProtocolHandler.pm @@ -1,6 +1,6 @@ package Slim::Plugin::Deezer::ProtocolHandler; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -576,7 +576,7 @@ sub getMetadataFor { my $song = $client->currentSongForUrl($url); if (!$song || !($url = $song->pluginData('radioTrackURL'))) { return { - title => $url =~ /flow\.dzr/ ? $client->string('PLUGIN_DEEZER_FLOW') : $client->string('PLUGIN_DEEZER_SMART_RADIO'), + title => ($url && $url =~ /flow\.dzr/) ? $client->string('PLUGIN_DEEZER_FLOW') : $client->string('PLUGIN_DEEZER_SMART_RADIO'), bitrate => '320k CBR', type => 'MP3 (Deezer)', icon => $icon, diff --git a/Slim/Plugin/DigitalInput/Plugin.pm b/Slim/Plugin/DigitalInput/Plugin.pm index 203bf4af8ed..8ded2411ca9 100644 --- a/Slim/Plugin/DigitalInput/Plugin.pm +++ b/Slim/Plugin/DigitalInput/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::DigitalInput::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/DigitalInput/ProtocolHandler.pm b/Slim/Plugin/DigitalInput/ProtocolHandler.pm index 89974600256..7ddb2588925 100644 --- a/Slim/Plugin/DigitalInput/ProtocolHandler.pm +++ b/Slim/Plugin/DigitalInput/ProtocolHandler.pm @@ -1,8 +1,6 @@ package Slim::Plugin::DigitalInput::ProtocolHandler; -# $Id - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/DigitalInput/strings.txt b/Slim/Plugin/DigitalInput/strings.txt index de33b6c4dae..b0610dad910 100644 --- a/Slim/Plugin/DigitalInput/strings.txt +++ b/Slim/Plugin/DigitalInput/strings.txt @@ -1,7 +1,5 @@ # String file for DigitalInput plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_DIGITAL_INPUT CS Digitální vstupy DA Digitale indgange diff --git a/Slim/Plugin/DnDPlay/HTML/Default/html/js-main-dd.html b/Slim/Plugin/DnDPlay/HTML/Default/html/js-main-dd.html deleted file mode 100644 index 58c5b304d43..00000000000 --- a/Slim/Plugin/DnDPlay/HTML/Default/html/js-main-dd.html +++ /dev/null @@ -1,20 +0,0 @@ -[% - PROCESS "html/js-main.html"; - - PROCESS jsString id='PLUGIN_DNDPLAY_NO_ITEMS' jsId=''; - PROCESS jsString id='PLAYLIST_NO_ITEMS_FOUND' jsId='noItemsFound'; - PROCESS jsString id='ADDING_TO_PLAYLIST' jsId=''; - - i = 0; -%] - - SqueezeJS.Strings['fileTooLarge'] = "[% fileTooLarge | html | replace('"', '\"') %]"; - - SqueezeJS.DnD = { - maxUploadSize: [% maxUploadSize %], - validTypeExtensions: '[% validTypeExtensions %]' - }; - -[% - INCLUDE "html/FileDnD.js"; -%] \ No newline at end of file diff --git a/Slim/Plugin/DnDPlay/HTML/Default/html/FileDnD.js b/Slim/Plugin/DnDPlay/HTML/Default/js-main-dd.js similarity index 90% rename from Slim/Plugin/DnDPlay/HTML/Default/html/FileDnD.js rename to Slim/Plugin/DnDPlay/HTML/Default/js-main-dd.js index b1a7b97fc82..ea8d7ee50c4 100644 --- a/Slim/Plugin/DnDPlay/HTML/Default/html/FileDnD.js +++ b/Slim/Plugin/DnDPlay/HTML/Default/js-main-dd.js @@ -1,3 +1,16 @@ +[% + PROCESS jsString id='PLUGIN_DNDPLAY_NO_ITEMS' jsId=''; + PROCESS jsString id='PLAYLIST_NO_ITEMS_FOUND' jsId='noItemsFound'; + PROCESS jsString id='ADDING_TO_PLAYLIST' jsId=''; +%] + +SqueezeJS.Strings['fileTooLarge'] = "[% fileTooLarge | html | replace('"', '\"') %]"; + +SqueezeJS.DnD = { + maxUploadSize: [% maxUploadSize %], + validTypeExtensions: '[% validTypeExtensions %]' +}; + if (window.File && window.FileList) { FileDnD = { queue: new Array, @@ -72,7 +85,7 @@ if (window.File && window.FileList) { this.showBriefly(SqueezeJS.string('adding_to_playlist') + ' ' + file.name); SqueezeJS.Controller.playerRequest({ - params: ['playlist', (action || 'add') + 'match', 'name:' + file.name, 'size:' + file.size, 'timestamp:' + Math.floor(file.lastModifiedDate.getTime() / 1000), 'type:' + file.type], + params: ['playlist', (action || 'add') + 'match', 'name:' + file.name, 'size:' + (file.size || 0), 'timestamp:' + Math.floor(file.lastModified / 1000), 'type:' + (file.type || 'unk')], success: function(response){ if (!this.statusUpdater) { SqueezeJS.Controller.getStatus(); @@ -155,7 +168,7 @@ if (window.File && window.FileList) { formdata.append('name', file.name); formdata.append('size', file.size); formdata.append('type', file.type); - formdata.append('timestamp', Math.floor(file.lastModifiedDate.getTime() / 1000)) + formdata.append('timestamp', Math.floor(file.lastModified / 1000)) if (file.key) formdata.append('key', file.key); diff --git a/Slim/Plugin/DnDPlay/Plugin.pm b/Slim/Plugin/DnDPlay/Plugin.pm index 9067f5029e6..0852dd24a47 100644 --- a/Slim/Plugin/DnDPlay/Plugin.pm +++ b/Slim/Plugin/DnDPlay/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::DnDPlay::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -43,14 +43,16 @@ sub initPlugin { require Slim::Plugin::DnDPlay::Settings; Slim::Plugin::DnDPlay::Settings->new(); - # this handler hijacks the default handler for js-main, to inject the D'n'd code - Slim::Web::Pages->addPageFunction("js-main\.html", sub { + Slim::Web::Pages->addPageFunction("js-main-dd.js", sub { my $params = $_[1]; $params->{maxUploadSize} = MAX_UPLOAD_SIZE; $params->{fileTooLarge} = string('PLUGIN_DNDPLAY_FILE_TOO_LARGE', '{0}', '{1}'); $params->{validTypeExtensions} = '\.(' . join('|', Slim::Music::Info::validTypeExtensions()) . ')$'; - Slim::Web::HTTP::filltemplatefile('html/js-main-dd.html', $params); + Slim::Web::HTTP::filltemplatefile('js-main-dd.js', $params); }); + + require Slim::Web::Pages::JS; + Slim::Web::Pages::JS->addJSFunction('js-main', 'js-main-dd.js'); } # the file upload is handled through a custom request handler, dealing with multi-part POST requests diff --git a/Slim/Plugin/DnDPlay/Settings.pm b/Slim/Plugin/DnDPlay/Settings.pm index 0191739e0ce..bfe3e7ba785 100644 --- a/Slim/Plugin/DnDPlay/Settings.pm +++ b/Slim/Plugin/DnDPlay/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::DnDPlay::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/DnDPlay/strings.txt b/Slim/Plugin/DnDPlay/strings.txt index 325414fa606..e5b175f8c5a 100644 --- a/Slim/Plugin/DnDPlay/strings.txt +++ b/Slim/Plugin/DnDPlay/strings.txt @@ -29,7 +29,7 @@ PLUGIN_DNDPLAY_NO_ITEMS PLUGIN_DNDPLAY_NO_PLAYER_CONNECTED DE Sie können ohne Player nichts wiedergeben. EN Cannot play music without a player connected. - NL Kan zonder muzieksysteem niets afspelen. + NL Kan zonder muziekspeler niets afspelen. NO Kan ikke spille musikk uten en tilkoblet spiller. PL Nie można uruchomić odtwarzania bez podpiętego odtwarzacza. diff --git a/Slim/Plugin/DontStopTheMusic/Plugin.pm b/Slim/Plugin/DontStopTheMusic/Plugin.pm index 70cbc52128a..2cbdc2db839 100644 --- a/Slim/Plugin/DontStopTheMusic/Plugin.pm +++ b/Slim/Plugin/DontStopTheMusic/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::DontStopTheMusic::Plugin; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -180,6 +180,7 @@ sub onPlaylistChange { ( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::RandomPlay::Plugin') && Slim::Plugin::RandomPlay::Plugin::active($client) ) || ( Slim::Utils::PluginManager->isEnabled('Plugins::SugarCube::Plugin') && preferences('plugin.SugarCube')->client($client)->get('sugarcube_status') ) ) { + $log->warn("Found RandomPlay or SugarCube active - I'm not going to interfere with them."); return; } @@ -228,7 +229,6 @@ sub dontStopTheMusic { my $numTracks = $prefs->get('newtracks') || MIN_TRACKS_LEFT; - # TODO - don't run twice, set a flag if ($songsRemaining < $numTracks) { # don't continue if the last item in the queue is a radio station or similar if ( my $handler = Slim::Player::ProtocolHandlers->handlerForURL( $client->playingSong()->track->url ) ) { @@ -251,6 +251,8 @@ sub dontStopTheMusic { if ( my $handler = $class->getHandler($client) ) { $client->pluginData( active => 1 ); + + Slim::Player::Playlist::preserveShuffleOrder($client); $handler->( $client, sub { my ($client, $tracks) = @_; @@ -374,6 +376,11 @@ sub getMixableProperties { sub getMixablePropertiesFromTrack { my ($class, $client, $track) = @_; + + # sometimes we would only get a URL - try to get the object instead + if (!blessed $track && Slim::Music::Info::isURL($track)) { + $track = Slim::Schema->objectForUrl($track); + } return unless $client && blessed $track; diff --git a/Slim/Plugin/DontStopTheMusic/Settings.pm b/Slim/Plugin/DontStopTheMusic/Settings.pm index 10d6d9b8c4a..1a050d1f54f 100644 --- a/Slim/Plugin/DontStopTheMusic/Settings.pm +++ b/Slim/Plugin/DontStopTheMusic/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::DontStopTheMusic::Settings; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -38,7 +38,7 @@ sub prefs { sub handler { my ($class, $client, $paramRef) = @_; - $client = $client->master, + $client = $client->master if $client; $paramRef->{handlers} = Slim::Plugin::DontStopTheMusic::Plugin::getSortedHandlerTokens($client); diff --git a/Slim/Plugin/ExtendedBrowseModes/Libraries.pm b/Slim/Plugin/ExtendedBrowseModes/Libraries.pm index 473d587d924..ab69ca4324a 100644 --- a/Slim/Plugin/ExtendedBrowseModes/Libraries.pm +++ b/Slim/Plugin/ExtendedBrowseModes/Libraries.pm @@ -1,6 +1,6 @@ package Slim::Plugin::ExtendedBrowseModes::Libraries; -# Logitech Media Server Copyright 2001-2014 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/ExtendedBrowseModes/PlayerSettings.pm b/Slim/Plugin/ExtendedBrowseModes/PlayerSettings.pm index ff1762b553b..4b74428bf4f 100644 --- a/Slim/Plugin/ExtendedBrowseModes/PlayerSettings.pm +++ b/Slim/Plugin/ExtendedBrowseModes/PlayerSettings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::ExtendedBrowseModes::PlayerSettings; -# Logitech Media Server Copyright 2001-2014 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/ExtendedBrowseModes/Plugin.pm b/Slim/Plugin/ExtendedBrowseModes/Plugin.pm index 2a2f33531da..162f115c1ff 100644 --- a/Slim/Plugin/ExtendedBrowseModes/Plugin.pm +++ b/Slim/Plugin/ExtendedBrowseModes/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::ExtendedBrowseModes::Plugin; -# Logitech Media Server Copyright 2001-2014 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -523,7 +523,7 @@ sub _hitlist { if (main::STATISTICS) { Slim::Menu::BrowseLibrary::_tracks( $client, sub { my ($result) = @_; - my $isWeb = $args->{isControl} && !$client->controlledBy; + my $isWeb = $args->{isControl} && !($client && $client->controlledBy); $result->{items} = [ map { diff --git a/Slim/Plugin/ExtendedBrowseModes/Settings.pm b/Slim/Plugin/ExtendedBrowseModes/Settings.pm index 1cdf284f19d..86dfe312b4d 100644 --- a/Slim/Plugin/ExtendedBrowseModes/Settings.pm +++ b/Slim/Plugin/ExtendedBrowseModes/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::ExtendedBrowseModes::Settings; -# Logitech Media Server Copyright 2001-2014 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/Extensions/HTML/EN/plugins/Extensions/settings/basic.html b/Slim/Plugin/Extensions/HTML/EN/plugins/Extensions/settings/basic.html index c10acb51e93..7c4a9d0ab60 100644 --- a/Slim/Plugin/Extensions/HTML/EN/plugins/Extensions/settings/basic.html +++ b/Slim/Plugin/Extensions/HTML/EN/plugins/Extensions/settings/basic.html @@ -85,7 +85,7 @@
- + diff --git a/Slim/Plugin/Extensions/Plugin.pm b/Slim/Plugin/Extensions/Plugin.pm index b2282fd5247..75dce30dbb6 100644 --- a/Slim/Plugin/Extensions/Plugin.pm +++ b/Slim/Plugin/Extensions/Plugin.pm @@ -30,7 +30,7 @@ package Slim::Plugin::Extensions::Plugin; # # # Applet and Plugin entries are of the form: -# +# # # English Title # Deutscher Titel @@ -65,14 +65,14 @@ package Slim::Plugin::Extensions::Plugin; # title - contains localisations for the title of the applet (optional - uses name if not defined) # desc - localised description of the applet or plugin (optional) # changes - localised change log of the applet or plugin (optional) -# link - (plugin only) url for web page describing the plugin in more detail +# link - (plugin only) url for web page describing the plugin in more detail # creator - identify of author(s) # email - email address of authors # url - url for the applet/plugin itself, this sould be a zip file # sha - (plugin only) sha1 digest of the zip file which is verifed before the zip is extracted # # Wallpaper and sound entries can include all of the above elements, but the minimal definition is: -# +# # # # @@ -104,7 +104,7 @@ my $prefs = preferences('plugin.extensions'); $prefs->init({ repos => [], plugin => {}, auto => 0 }); -$prefs->migrate(2, +$prefs->migrate(2, sub { # find any plugins already installed via previous version of extension downloader and save as selected # this should avoid trying to remove existing plugins when this version is first loaded @@ -127,7 +127,7 @@ $prefs->migrate(3, my %repos = ( # default repos mapped to weight which defines the order they are sorted in - 'http://repos.squeezecommunity.org/extensions.xml' => 1, + 'https://github.com/LMS-Community/lms-plugin-repository/raw/master/extensions.xml' => 1, ); sub initPlugin { @@ -144,31 +144,31 @@ sub initPlugin { for my $repo ( @{$prefs->get('repos')} ) { $class->addRepo({ repo => $repo }); } - + Slim::Plugin::Extensions::Settings->new; # clean out plugin entries for plugins which are manually installed # this can happen if a developer moves an automatically installed plugin to a manually installed location my $installPlugins = $prefs->get('plugin'); my $loadedPlugins = Slim::Utils::PluginManager->allPlugins; - + for my $plugin (keys %$installPlugins) { - + if ($loadedPlugins->{ $plugin } && $loadedPlugins->{ $plugin }->{'basedir'} !~ /InstalledPlugins/) { - + $log->warn("removing $plugin from install list as it is already manually installed"); - + delete $installPlugins->{ $plugin }; - + $prefs->set('plugin', $installPlugins); } - + # a plugin could have failed to download (Thanks Google for taking down googlecode.com!...) - let's not re-try to install it elsif ( !$loadedPlugins->{ $plugin } ) { $log->warn("$plugin failed to download or install in some other way. Please try again."); - + delete $installPlugins->{ $plugin }; - + $prefs->set('plugin', $installPlugins); } } @@ -227,8 +227,8 @@ sub appsQuery { for my $repo (keys %repos) { getExtensions({ - 'name' => $repo, - 'type' => $args->{'type'}, + 'name' => $repo, + 'type' => $args->{'type'}, 'target' => $args->{'targetPlat'} || Slim::Utils::OSDetect::OS(), 'version'=> $args->{'targetVers'} || $::VERSION, 'lang' => $args->{'lang'} || $Slim::Utils::Strings::currentLang, @@ -278,6 +278,64 @@ sub _appsQueryCB { $request->setStatusDone(); } +sub getCurrentPlugins { + my $plugins = Slim::Utils::PluginManager->allPlugins; + my $states = preferences('plugin.state'); + + my $hide = {}; + my $current = {}; + + # create entries for built in plugins and those already installed + my @active; + my @inactive; + + for my $plugin (keys %$plugins) { + + if ( main::NOMYSB && ($plugins->{$plugin}->{needsMySB} && $plugins->{$plugin}->{needsMySB} !~ /false|no/i) ) { + main::DEBUGLOG && $log->debug("Skipping plugin: $plugin - requires mysqueezebox.com, but support for mysqueezebox.com is disabled."); + next; + } + + my $entry = $plugins->{$plugin}; + + # don't show enforced plugins + next if $entry->{'enforce'}; + + my $state = $states->get($plugin); + + my $entry = { + name => $plugin, + title => Slim::Utils::Strings::getString($entry->{'name'}), + desc => Slim::Utils::Strings::getString($entry->{'description'}), + error => Slim::Utils::PluginManager->getErrorString($plugin), + creator => $entry->{'creator'}, + email => $entry->{'email'}, + homepage=> $entry->{'homepageURL'}, + version => $entry->{'version'}, + settings=> Slim::Utils::PluginManager->isEnabled($entry->{'module'}) ? $entry->{'optionsURL'} : undef, + manual => $entry->{'basedir'} !~ /InstalledPlugins/ ? 1 : 0, + enforce => $entry->{'enforce'}, + }; + + if ($state =~ /enabled/) { + + push @active, $entry; + + if (!$entry->{'manual'}) { + $current->{ $plugin } = $entry->{'version'}; + } + + } elsif ($state =~ /disabled/) { + + push @inactive, $entry; + } + + $hide->{$plugin} = 1; + } + + return ($current, \@active, \@inactive, $hide); +} + sub findUpdates { my $results = shift; my $current = shift; @@ -304,7 +362,7 @@ sub findUpdates { if (!defined $current->{ $app } || Slim::Utils::Versions->compareVersions($apps->{ $app }->{'version'}, $current->{ $app }) > 0){ - main::INFOLOG && $log->info("$app action install version " . $apps->{ $app }->{'version'} . + main::INFOLOG && $log->info("$app action install version " . $apps->{ $app }->{'version'} . ($current->{ $app } ? (" from " . $current->{ $app }) : '')); $actions->{ $app } = { action => 'install', url => $apps->{ $app }->{'url'}, sha => $apps->{ $app }->{'sha'} }; @@ -340,11 +398,11 @@ sub getExtensions { if ( my $cached = $cache->get( $args->{'name'} . '_XML' ) ) { main::DEBUGLOG && $log->debug("using cached extensions xml $args->{name}"); - + _parseXML($args, $cached); } else { - + main::DEBUGLOG && $log->debug("fetching extensions xml $args->{name}"); Slim::Networking::Repositories->get( @@ -362,19 +420,19 @@ sub _parseResponse { my $xml = {}; - eval { + eval { $xml = XMLin($http->content, SuppressEmpty => undef, - KeyAttr => { - title => 'lang', - desc => 'lang', + KeyAttr => { + title => 'lang', + desc => 'lang', changes => 'lang' }, ContentKey => '-content', GroupTags => { - applets => 'applet', - sounds => 'sound', - wallpapers => 'wallpaper', + applets => 'applet', + sounds => 'sound', + wallpapers => 'wallpaper', plugins => 'plugin', patches => 'patch', }, @@ -389,7 +447,7 @@ sub _parseResponse { } else { my $cache = Slim::Utils::Cache->new; - + $cache->set( $args->{'name'} . '_XML', $xml, 300 ); } @@ -425,7 +483,7 @@ sub _parseXML { my $debug = main::DEBUGLOG && $log->is_debug; my $repoTitle; - + $debug && $log->debug("searching $args->{name} for type: $type target: $target version: $version"); my @res = (); @@ -454,7 +512,7 @@ sub _parseXML { }; $new->{'sha'} = $entry->{'sha'} if $entry->{'sha'}; - + $debug && $log->debug("entry $new->{name} vers: $new->{version} url: $new->{url}"); if ($details) { @@ -470,10 +528,13 @@ sub _parseXML { $new->{'desc'} = $entry->{'desc'}->{ $lang } || $entry->{'desc'}->{ 'EN' }; } $new->{desc} = '' if ref $new->{desc}; - + if ($entry->{'changes'} && ref $entry->{'changes'} eq 'HASH') { $new->{'changes'} = $entry->{'changes'}->{ $lang } || $entry->{'changes'}->{ 'EN' }; } + elsif (!ref $entry->{changes}) { + $new->{changes} = $entry->{changes}; + } $new->{changes} = '' if ref $new->{changes}; $new->{'link'} = $entry->{'link'} if $entry->{'link'}; @@ -493,22 +554,22 @@ sub _parseXML { if ($details) { - if ( $xml->{details} && $xml->{details}->{title} + if ( $xml->{details} && $xml->{details}->{title} && ($xml->{details}->{title}->{$lang} || $xml->{details}->{title}->{EN}) ) { - + $repoTitle = $xml->{details}->{title}->{$lang} || $xml->{details}->{title}->{EN}; - + } else { - + # fall back to repo's URL if no title is provided $repoTitle = $args->{name}; } - + $info = { 'name' => $args->{'name'}, 'title' => $repoTitle, }; - + } $debug && $log->debug("found " . scalar(@res) . " extensions"); diff --git a/Slim/Plugin/Extensions/Settings.pm b/Slim/Plugin/Extensions/Settings.pm index 91ad360feaf..9ee9093b8e0 100644 --- a/Slim/Plugin/Extensions/Settings.pm +++ b/Slim/Plugin/Extensions/Settings.pm @@ -9,7 +9,7 @@ use Slim::Utils::OSDetect; use Digest::MD5; -use constant MAX_DOWNLOAD_WAIT => 20; +use constant MAX_DOWNLOAD_WAIT => 120; my $prefs = preferences('plugin.extensions'); my $log = logger('plugin.extensions'); @@ -59,7 +59,7 @@ sub handler { $changed = 1; } } - + for my $repo (keys %current) { if (!$new{$repo}) { Slim::Plugin::Extensions::Plugin->removeRepo({ repo => $repo }); @@ -90,7 +90,7 @@ sub handler { } } } - + $prefs->set('plugin', $plugin) if $changed; } @@ -101,10 +101,10 @@ sub handler { for my $repo (keys %$repos) { Slim::Plugin::Extensions::Plugin::getExtensions({ - 'name' => $repo, - 'type' => 'plugin', + 'name' => $repo, + 'type' => 'plugin', 'target' => Slim::Utils::OSDetect::OS(), - 'version'=> $::VERSION, + 'version'=> $::VERSION, 'lang' => $Slim::Utils::Strings::currentLang, 'details'=> 1, 'cb' => \&_getReposCB, @@ -131,23 +131,26 @@ sub _getReposCB { } if ( --$data->{'remaining'} <= 0 ) { - + my $pageInfo = $class->_addInfo($client, $params, $data); - + my $finalize; - my $timeout = MAX_DOWNLOAD_WAIT; - + my $timeout = Time::HiRes::time() + MAX_DOWNLOAD_WAIT; + $finalize = sub { Slim::Utils::Timers::killTimers(undef, $finalize); - + # if a plugin is still being downloaded, wait a bit longer, or the user might restart the server before we're done - if ( $timeout-- > 0 && Slim::Utils::PluginDownloader->downloading ) { + if ( Time::HiRes::time() <= $timeout && Slim::Utils::PluginDownloader->downloading ) { Slim::Utils::Timers::setTimer(undef, time() + 1, $finalize); - + main::DEBUGLOG && $log->is_debug && $log->debug("PluginDownloader is still busy - waiting a little longer..."); return; } - + elsif ( Time::HiRes::time() > $timeout ) { + $log->warn("Plugin download timed out"); + } + $callback->($client, $params, $pageInfo, @$args); }; @@ -158,63 +161,10 @@ sub _getReposCB { sub _addInfo { my ($class, $client, $params, $data) = @_; - my $plugins = Slim::Utils::PluginManager->allPlugins; - my $states = preferences('plugin.state'); - - my $hide = {}; - my $current = {}; - - # create entries for built in plugins and those already installed - my @active; - my @inactive; - my @updates; - - for my $plugin (keys %$plugins) { - - if ( main::NOMYSB && $plugins->{$plugin}->{needsMySB} ) { - main::DEBUGLOG && $log->debug("Skipping plugin: $plugin - requires mysqueezebox.com, but support for mysqueezebox.com is disabled."); - next; - } - - my $entry = $plugins->{$plugin}; - - # don't show enforced plugins - next if $entry->{'enforce'}; - - my $state = $states->get($plugin); - - my $entry = { - name => $plugin, - title => Slim::Utils::Strings::getString($entry->{'name'}), - desc => Slim::Utils::Strings::getString($entry->{'description'}), - error => Slim::Utils::PluginManager->getErrorString($plugin), - creator => $entry->{'creator'}, - email => $entry->{'email'}, - homepage=> $entry->{'homepageURL'}, - version => $entry->{'version'}, - settings=> Slim::Utils::PluginManager->isEnabled($entry->{'module'}) ? $entry->{'optionsURL'} : undef, - manual => $entry->{'basedir'} !~ /InstalledPlugins/ ? 1 : 0, - enforce => $entry->{'enforce'}, - }; - - if ($state =~ /enabled/) { - - push @active, $entry; - - if (!$entry->{'manual'}) { - $current->{ $plugin } = $entry->{'version'}; - } - - } elsif ($state =~ /disabled/) { - - push @inactive, $entry; - } - - $hide->{$plugin} = 1; - } + my ($current, $active, $inactive, $hide) = Slim::Plugin::Extensions::Plugin::getCurrentPlugins(); my @results = sort { $a->{'weight'} != $b->{'weight'} ? - $a->{'weight'} <=> $b->{'weight'} : + $a->{'weight'} <=> $b->{'weight'} : $a->{'title'} cmp $b->{'title'} } values %{$data->{'results'}}; my @res; @@ -226,6 +176,7 @@ sub _addInfo { # find update actions and handle my $actions = Slim::Plugin::Extensions::Plugin::findUpdates(\@res, $current, $prefs->get('plugin'), 'info'); + my @updates; for my $plugin (keys %$actions) { @@ -248,7 +199,7 @@ sub _addInfo { } $hide->{$plugin} = 1; - + } elsif ($entry->{'action'} eq 'uninstall') { main::INFOLOG && $log->info("uninstalling $plugin"); @@ -258,7 +209,7 @@ sub _addInfo { } # prune out duplicate entries, favour favour higher version numbers - + # pass 1 - find the higher version numbers my $max = {}; @@ -286,8 +237,8 @@ sub _addInfo { my @repos = ( @{$prefs->get('repos')}, '' ); $params->{'updates'} = \@updates; - $params->{'active'} = \@active; - $params->{'inactive'} = \@inactive; + $params->{'active'} = $active; + $params->{'inactive'} = $inactive; $params->{'avail'} = \@results; $params->{'repos'} = \@repos; $params->{'auto'} = $prefs->get('auto'); diff --git a/Slim/Plugin/Favorites/HTML/EN/plugins/Favorites/index.html b/Slim/Plugin/Favorites/HTML/EN/plugins/Favorites/index.html index 15449899c2f..1470ba5e0dd 100644 --- a/Slim/Plugin/Favorites/HTML/EN/plugins/Favorites/index.html +++ b/Slim/Plugin/Favorites/HTML/EN/plugins/Favorites/index.html @@ -11,7 +11,7 @@ - + [% IF useAJAX %] @@ -29,7 +29,7 @@ - +
[% "ENABLE" | string %][% IF type == 'update' %][% "UPDATE" | string %][% ELSE %][% "ENABLE" | string %][% END %] [% "NAME" | string %] [% "DESCRIPTION" | string %] [% "PLUGIN_EXTENSIONS_AUTHOR" | string %] @@ -55,7 +55,7 @@

- + [% IF useAJAX %] @@ -217,7 +217,7 @@

[% "PLUGIN_FAVORITES_PASTE" | string %] [ [% deleted %] ] + [%- END %]>[% "PLUGIN_FAVORITES_PASTE" | string %] [ [% deleted | html %] ] [% END %] [% IF useAJAX %][% END %] diff --git a/Slim/Plugin/Favorites/Opml.pm b/Slim/Plugin/Favorites/Opml.pm index d19f0eb1285..c24cac4a3b5 100644 --- a/Slim/Plugin/Favorites/Opml.pm +++ b/Slim/Plugin/Favorites/Opml.pm @@ -2,7 +2,6 @@ package Slim::Plugin::Favorites::Opml; # Base class for editing opml files - front end to XMLin and XMLout -# $Id$ use strict; diff --git a/Slim/Plugin/Favorites/OpmlFavorites.pm b/Slim/Plugin/Favorites/OpmlFavorites.pm index 753060d4f51..78e82c25076 100644 --- a/Slim/Plugin/Favorites/OpmlFavorites.pm +++ b/Slim/Plugin/Favorites/OpmlFavorites.pm @@ -2,7 +2,6 @@ package Slim::Plugin::Favorites::OpmlFavorites; # An opml based favorites handler -# $Id$ use strict; diff --git a/Slim/Plugin/Favorites/Playlist.pm b/Slim/Plugin/Favorites/Playlist.pm index f09b849d531..50cff6dd995 100644 --- a/Slim/Plugin/Favorites/Playlist.pm +++ b/Slim/Plugin/Favorites/Playlist.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Favorites::Playlist; -# $Id$ # Class to allow importing of playlist formats understood by Logitech Media Server into opml files diff --git a/Slim/Plugin/Favorites/Plugin.pm b/Slim/Plugin/Favorites/Plugin.pm index fcfc2e21766..52042d817c1 100644 --- a/Slim/Plugin/Favorites/Plugin.pm +++ b/Slim/Plugin/Favorites/Plugin.pm @@ -7,7 +7,7 @@ package Slim::Plugin::Favorites::Plugin; # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2005-2016 Logitech. +# Logitech Media Server Copyright 2005-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -42,6 +42,12 @@ my $log = logger('favorites'); my $prefs = preferences('plugin.favorites'); +# make sure the value is defined, otherwise it would be enabled again +$prefs->setChange( sub { + $prefs->set($_[0], 0) unless defined $_[1]; +}, 'registerDSTM' ); + + # support multiple edditing sessions at once - indexed by sessionId. [Default to favorites editting] my $nextSession = 2; # session id 1 = favorites tie my %sessions, 'Tie::Cache::LRU', 4; @@ -501,7 +507,7 @@ sub indexHandler { # cancel on a new item - remove it splice @$level, $indexLevel, 1; - } elsif ($params->{'entrytitle'}) { + } elsif ($params->{'entrytitle'} && !$params->{'cancel'}) { # editted item - modify including possibly changing type my $entry = @$level[$indexLevel]; @@ -1011,7 +1017,7 @@ sub _objectInfoHandler { }; my $title; - if ($objectType eq 'artist') { + if ($objectType && $objectType eq 'artist') { $title = $obj->name; } else { $title = $obj->title; diff --git a/Slim/Plugin/Favorites/Settings.pm b/Slim/Plugin/Favorites/Settings.pm index ddb63a1fc22..15bc25df58b 100644 --- a/Slim/Plugin/Favorites/Settings.pm +++ b/Slim/Plugin/Favorites/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::Favorites::Settings; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/Favorites/SqueezeNetwork.pm b/Slim/Plugin/Favorites/SqueezeNetwork.pm deleted file mode 100644 index 14931f9a7a2..00000000000 --- a/Slim/Plugin/Favorites/SqueezeNetwork.pm +++ /dev/null @@ -1,194 +0,0 @@ -package Slim::Plugin::Favorites::SqueezeNetwork; - -# Pull favorites from the SN database - -# $Id$ - -use strict; - -use Scalar::Util qw(blessed); - -use Slim::Utils::Log; - -my $log = logger('favorites'); - -sub new { - my ( $class, $client ) = @_; - - my $self = bless { - userid => $client->playerData->userid->id, - }, $class; - - return $self; -} - -# Not part of the Favorites API, but needed by the Alarm Clock -sub all { - my $self = shift; - - my @favs = SDI::Service::Model::Favorite->search( - userid => $self->{userid}, - { - order_by => 'num' - } - ); - - my $all = []; - - for my $fav ( @favs ) { - push @{$all}, { - title => $fav->title, - url => $fav->url, - }; - } - - return $all; -} - -sub add { - my ( $self, $url, $title ) = @_; - - if ( blessed($url) && $url->can('url') ) { - $url = $url->url; - } - - # See if this favorite already exists - my ($fav) = SDI::Service::Model::Favorite->search( - userid => $self->{userid}, - url => $url, - ); - - if ( !$fav ) { - my $max = SDI::Service::Model::Favorite->max( $self->{userid} ); - - $fav = SDI::Service::Model::Favorite->find_or_create( { - userid => $self->{userid}, - url => $url, - title => $title, - num => $max + 1, - } ); - - main::DEBUGLOG && $log->debug( "Added favorite $title ($url) at index " . ( $max + 1 ) ); - } - else { - $fav->title( $title ); - $fav->update; - - main::DEBUGLOG && $log->debug("Favorite $url already exists"); - } - - # NOMEMCACHE - # Slim::Utils::Cache->new->set( 'favorites_last_mod_' . $self->{userid}, time(), 86400 * 30 ); - - return $fav->num - 1; -} - -sub hasUrl { - my ( $self, $url ) = @_; - - my $fav = SDI::Service::Model::Favorite->findByUserAndURL( $self->{userid}, $url ); - - if ( $fav ) { - main::DEBUGLOG && $log->debug( "User has favorite $url" ); - return 1; - } - - return; -} - -sub findUrl { - my ( $self, $url ) = @_; - - my $fav = SDI::Service::Model::Favorite->findByUserAndURL( $self->{userid}, $url ); - - if ( $fav ) { - main::DEBUGLOG && $log->is_debug && $log->debug( "User has favorite $url at index " . $fav->num ); - return $fav->num - 1; - } - - return; -} - -sub deleteUrl { - my ( $self, $url ) = @_; - - my $fav = SDI::Service::Model::Favorite->findByUserAndURL( $self->{userid}, $url ); - - if ( $fav ) { - SDI::Service::Model::Favorite->deleteAndRenumber( $self->{userid}, $fav->id ); - - main::DEBUGLOG && $log->debug( "Deleted favorite for $url" ); - - # NOMEMCACHE - # Slim::Utils::Cache->new->set( 'favorites_last_mod_' . $self->{userid}, time(), 86400 * 30 ); - - return 1; - } - - return; -} - -sub deleteIndex { - my ( $self, $index ) = @_; - - my ($fav) = SDI::Service::Model::Favorite->search( - userid => $self->{userid}, - num => $index + 1, - ); - - if ( $fav ) { - main::DEBUGLOG && $log->is_debug && $log->debug( "Deleted favorite index $index (" . $fav->url . ")" ); - - SDI::Service::Model::Favorite->deleteAndRenumber( $self->{userid}, $fav->id ); - - Slim::Utils::Cache->new->set( 'favorites_last_mod_' . $self->{userid}, time(), 86400 * 30 ); - - return 1; - } - - return; -} - -sub entry { - my ( $self, $index ) = @_; - - my ($fav) = SDI::Service::Model::Favorite->search( - userid => $self->{userid}, - num => $index + 1, - ); - - if ( $fav ) { - return { - title => $fav->title, - text => $fav->title, - URL => $fav->url, - type => 'audio', - }; - } - - return; -} - -# legacy method to support migration to presets -sub hotkeys { - my $self = shift; - - my @keys; - - for my $key (1..9,0) { - my ($item) = SDI::Service::Model::Favorite->search( - userid => $self->{userid}, - hotkey => $key, - ); - push @keys, { - key => $key, - used => $item ? 1 : 0, - title => $item ? $item->title : undef, - index => $item ? $item->num - 1 : undef, - }; - } - - return \@keys; -} - -1; diff --git a/Slim/Plugin/Favorites/strings.txt b/Slim/Plugin/Favorites/strings.txt index 85f20617f8f..9d26979e7da 100644 --- a/Slim/Plugin/Favorites/strings.txt +++ b/Slim/Plugin/Favorites/strings.txt @@ -598,7 +598,7 @@ PLUGIN_FAVORITES_DONT_BROWSEDB PLUGIN_FAVORITES_DONT_BROWSEDB_ON DE Beim Anwählen eines Eintrages Favoriten abspielen, nicht durchsuchen (altes Verhalten) - EN Don't browse items whene selected, just play them (old behaviour) + EN Don't browse items when selected, just play them (old behaviour) NL Niet doorbrowsen op een geselecteerd item, maar speel af (oude gedrag) NO Spill av valgt oppføring, ikke bla videre (gammel oppførsel) PL Nie przeglądaj utworów, po prostu je odtwarzaj (klasyczne zachowanie) diff --git a/Slim/Plugin/Flickr/HTML/EN/plugins/Flickr/html/images/icon.png b/Slim/Plugin/Flickr/HTML/EN/plugins/Flickr/html/images/icon.png deleted file mode 100644 index 2891b627510..00000000000 Binary files a/Slim/Plugin/Flickr/HTML/EN/plugins/Flickr/html/images/icon.png and /dev/null differ diff --git a/Slim/Plugin/Flickr/HTML/EN/plugins/Flickr/html/images/icon_40x40_m.png b/Slim/Plugin/Flickr/HTML/EN/plugins/Flickr/html/images/icon_40x40_m.png deleted file mode 100644 index 4d512cc9d54..00000000000 Binary files a/Slim/Plugin/Flickr/HTML/EN/plugins/Flickr/html/images/icon_40x40_m.png and /dev/null differ diff --git a/Slim/Plugin/Flickr/Plugin.pm b/Slim/Plugin/Flickr/Plugin.pm deleted file mode 100644 index 7056498ab97..00000000000 --- a/Slim/Plugin/Flickr/Plugin.pm +++ /dev/null @@ -1,158 +0,0 @@ -package Slim::Plugin::Flickr::Plugin; - -# $Id$ - -use strict; -use base qw(Slim::Plugin::OPMLBased); - -use JSON::XS::VersionOneAndTwo; -use URI::Escape qw(uri_escape_utf8); - -use Slim::Networking::SqueezeNetwork; - -# SP screensavers -# XXX these user-required screensavers should be defined some other way -my @savers = qw( - mine - contacts - favorites - interesting - recent -); - -sub initPlugin { - my $class = shift; - - $class->SUPER::initPlugin( - feed => Slim::Networking::SqueezeNetwork->url( '/api/flickr/v1/opml' ), - tag => 'flickr', - is_app => 1, - ); - - # Track Info item - Slim::Menu::TrackInfo->registerInfoProvider( flickr => ( - after => 'middle', - func => \&trackInfoMenu, - ) ); -} - -# Don't add this item to any menu -sub playerMenu { } - -sub initCLI { - my ( $class, %args ) = @_; - - $class->SUPER::initCLI( %args ); - - for my $saver ( @savers ) { - Slim::Control::Request::addDispatch( - [ $args{tag}, 'screensaver_' . $saver ], - [ 1, 1, 1, sub { - _screensaver_request( "/api/flickr/v1/screensaver/$saver", @_ ); - } ] - ); - } -} - -# Extend initJive to setup screensavers -sub initJive { - my ( $class, %args ) = @_; - - my $menu = $class->SUPER::initJive( %args ); - - return if !$menu; - - $menu->[0]->{screensavers} = []; - - for my $saver ( @savers ) { - push @{ $menu->[0]->{screensavers} }, { - cmd => [ $args{tag}, 'screensaver_' . $saver ], - stringToken => 'PLUGIN_FLICKR_SCREENSAVER_' . uc($saver), - }; - } - - return $menu; -} - -sub trackInfoMenu { - my ( $client, $url, $track, $remoteMeta ) = @_; - - return unless $client; - - # Only display on SP devices - return unless $client->isa('Slim::Player::SqueezePlay') || $client->controlledBy eq 'squeezeplay'; - - # Only show if in the app list - return unless $client->isAppEnabled('flickr'); - - my $artist = $track->remote ? $remoteMeta->{artist} : $track->artistName; - - if ( $artist ) { - my $snURL = '/api/flickr/v1/opml/context'; - $snURL .= '?artist=' . uri_escape_utf8($artist); - - return { - type => 'slideshow', - name => $client->string('PLUGIN_FLICKR_ON_FLICKR'), - url => Slim::Networking::SqueezeNetwork->url($snURL), - favorites => 0, - }; - } -} - -### Screensavers - -# Each call to a screensaver returns a new image + metadata to display -# { -# image => 'http://...', -# caption => 'text', -# } - -sub _screensaver_request { - my $url = shift; - my $request = shift; - my $client = $request->client; - - $url = Slim::Networking::SqueezeNetwork->url($url); - - my $http = Slim::Networking::SqueezeNetwork->new( - \&_screensaver_ok, - \&_screensaver_error, - { - client => $client, - request => $request, - timeout => 35, - }, - ); - - $http->get( $url ); - - $request->setStatusProcessing(); -} - -sub _screensaver_ok { - my $http = shift; - my $request = $http->params('request'); - - my $data = eval { from_json( $http->content ) }; - if ( $@ || $data->{error} || !$data->{image} ) { - $http->error( $@ || $data->{error} ); - _screensaver_error( $http ); - return; - } - - $request->addResult( data => [ $data ] ); - - $request->setStatusDone(); -} - -sub _screensaver_error { - my $http = shift; - my $error = $http->error; - my $request = $http->params('request'); - - # Not sure what status to use here - $request->setStatusBadParams(); -} - -1; diff --git a/Slim/Plugin/Flickr/install.xml b/Slim/Plugin/Flickr/install.xml deleted file mode 100644 index e46e0ef4454..00000000000 --- a/Slim/Plugin/Flickr/install.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - 0FD5B1E4-9CC7-4519-B958-01CBE03B54AB - PLUGIN_FLICKR_MODULE_NAME - Slim::Plugin::Flickr::Plugin - 1.0 - PLUGIN_FLICKR_MODULE_NAME - Logitech - enabled - true - http://www.mysqueezebox.com/appgallery/Flickr - plugins/Flickr/html/images/icon.png - 2 - - Logitech Media Server - 7.4 - * - - diff --git a/Slim/Plugin/Flickr/strings.txt b/Slim/Plugin/Flickr/strings.txt deleted file mode 100644 index 9c0e322d985..00000000000 --- a/Slim/Plugin/Flickr/strings.txt +++ /dev/null @@ -1,105 +0,0 @@ -PLUGIN_FLICKR_MODULE_NAME - CS Flickr - DA Flickr - DE Flickr - EN Flickr - ES Flickr - FI Flickr - FR Flickr - IT Flickr - NL Flickr - NO Flickr - PL Flickr - RU Flickr - SV Flickr - -PLUGIN_FLICKR_SCREENSAVER_MINE - CS Flickr: Moje obrázky - DA Flickr: Mine billeder - DE Flickr: Eigene Fotos - EN Flickr: My Photos - ES Flickr: Mis fotos - FI Flickr: My Photos - FR Flickr: mes photos - IT Flickr: Foto - NL Flickr: Mijn afbeeldingen - NO Flickr: Bilder - PL Flickr: moje zdjęcia - RU Flickr: My Photos - SV Flickr: My Photos - -PLUGIN_FLICKR_SCREENSAVER_CONTACTS - CS Flickr: Moje kontakty - DA Flickr: Mine kontaktpersoner - DE Flickr: Kontakte - EN Flickr: My Contacts - ES Flickr: Mis contactos - FI Flickr: My Contacts - FR Flickr: mes contacts - IT Flickr: Contatti - NL Flickr: Mijn contactpersonen - NO Flickr: Kontakter - PL Flickr: moje kontakty - RU Flickr: My Contacts - SV Flickr: My Contacts - -PLUGIN_FLICKR_SCREENSAVER_INTERESTING - CS Flickr: Zajímavé fotky - DA Flickr: Interessante billeder - DE Flickr: Interessanteste Fotos - EN Flickr: Interesting Photos - ES Flickr: fotos interesantes - FI Flickr: mielenkiintoisia valokuvia - FR Flickr: photos intéressantes - IT Flickr: foto di interesse - NL Flickr: interessante foto's - NO Flickr: Interessante bilder - PL Flickr: interesujące zdjęcia - RU Flickr: интересные фото - SV Flickr: Intressanta bilder - -PLUGIN_FLICKR_SCREENSAVER_RECENT - CS Flickr: Poslední fotky - DA Flickr: Seneste billeder - DE Flickr: Neueste Fotos - EN Flickr: Recent Photos - ES Flickr: fotos recientes - FI Flickr: uusimmat valokuvat - FR Flickr: photos récentes - IT Flickr: foto recenti - NL Flickr: recente foto's - NO Flickr: Nye bilder - PL Flickr: ostatnie zdjęcia - RU Flickr: последние фото - SV Flickr: Nya bilder - -PLUGIN_FLICKR_SCREENSAVER_FAVORITES - CS Flickr: Oblíbené položky - DA Flickr: Favoritter - DE Flickr: Favoriten - EN Flickr: Favorites - ES Flickr: Favoritos - FI Flickr: Favorites - FR Flickr: favoris - IT Flickr: Preferiti - NL Flickr: Favorieten - NO Flickr: Favoritter - PL Flickr: ulubione - RU Flickr: Favorites - SV Flickr: Favorites - -PLUGIN_FLICKR_ON_FLICKR - CS Na Flickr - DA På Flickr - DE Auf Flickr - EN On Flickr - ES En Flickr - FI Flickr-sivustolla - FR Sur Flickr - IT Su Flickr - NL Op Flickr - NO På Flickr - PL W serwisie Flickr - RU На Flickr - SV På Flickr - diff --git a/Slim/Plugin/FullTextSearch/Plugin.pm b/Slim/Plugin/FullTextSearch/Plugin.pm index b48099fce49..a0464643733 100644 --- a/Slim/Plugin/FullTextSearch/Plugin.pm +++ b/Slim/Plugin/FullTextSearch/Plugin.pm @@ -7,6 +7,9 @@ use Slim::Utils::Log; use Slim::Utils::Prefs; use Slim::Utils::Scanner::API; use Slim::Utils::Strings qw(string); +use Slim::Utils::Text; + +use constant CAN_FTS4 => Slim::Utils::Versions->compareVersions($DBD::SQLite::VERSION, 1.42) > 0; use constant BUILD_STEPS => 7; use constant FIRST_COLUMN => 2; @@ -14,61 +17,66 @@ use constant LARGE_RESULTSET => 500; use constant SQL_CREATE_TRACK_ITEM => q{ INSERT %s INTO fulltext (id, type, w10, w5, w3, w1) - SELECT tracks.id, 'track', + SELECT tracks.id, 'track', -- weight 10 - IFNULL(tracks.title, '') || ' ' || IFNULL(tracks.titlesearch, '') || ' ' || IFNULL(tracks.customsearch, '') || ' ' || IFNULL(tracks.musicbrainz_id, ''), + LOWER(IFNULL(tracks.title, '')) || ' ' || IFNULL(tracks.titlesearch, '') || ' ' || IFNULL(tracks.customsearch, ''), -- weight 5 - IFNULL(tracks.year, '') || ' ' || GROUP_CONCAT(albums.title, ' ') || ' ' || GROUP_CONCAT(albums.titlesearch, ' ') || ' ' || GROUP_CONCAT(genres.name, ' ') || ' ' || GROUP_CONCAT(genres.namesearch, ' '), + IFNULL(tracks.year, '') || ' ' || GROUP_CONCAT(albums.title, ' ') || ' ' || GROUP_CONCAT(albums.titlesearch, ' ') || ' ' + || GROUP_CONCAT(genres.name, ' ') || ' ' || GROUP_CONCAT(genres.namesearch, ' '), -- weight 3 - contributors create multiple hits, therefore only w3 - CONCAT_CONTRIBUTOR_ROLE(tracks.id, GROUP_CONCAT(contributor_track.contributor, ','), 'contributor_track') || ' ' || - IGNORE_CASE(comments.value) || ' ' || IGNORE_CASE(tracks.lyrics) || ' ' || IFNULL(tracks.content_type, '') || ' ' || CASE WHEN tracks.channels = 1 THEN 'mono' WHEN tracks.channels = 2 THEN 'stereo' END, + CONCAT_CONTRIBUTOR_ROLE(tracks.id, GROUP_CONCAT(contributor_track.contributor, ','), 'contributor_track') || ' ' + || IGNORE_CASE(comments.value) || ' ' || IGNORE_CASE(tracks.lyrics) || ' ' || IFNULL(tracks.content_type, '') || ' ' + || CASE WHEN tracks.channels = 1 THEN 'mono' WHEN tracks.channels = 2 THEN 'stereo' END, -- weight 1 - printf('%%i', tracks.bitrate) || ' ' || printf('%%ikbps', tracks.bitrate / 1000) || ' ' || IFNULL(tracks.samplerate, '') || ' ' || (round(tracks.samplerate, 0) / 1000) || ' ' || IFNULL(tracks.samplesize, '') || ' ' || replace(replace(tracks.url, '%%20', ' '), 'file://', '') - + printf('%%i', tracks.bitrate) || ' ' || printf('%%ikbps', tracks.bitrate / 1000) || ' ' || IFNULL(tracks.samplerate, '') || ' ' + || (round(tracks.samplerate, 0) / 1000) || ' ' || IFNULL(tracks.samplesize, '') || ' ' || replace(replace(tracks.url, '%%20', ' '), 'file://', '') || ' ' + || LOWER(IFNULL(tracks.musicbrainz_id, '')) + FROM tracks LEFT JOIN contributor_track ON contributor_track.track = tracks.id LEFT JOIN albums ON albums.id = tracks.album LEFT JOIN genre_track ON genre_track.track = tracks.id LEFT JOIN genres ON genres.id = genre_track.genre LEFT JOIN comments ON comments.track = tracks.id - + %s - + GROUP BY tracks.id; }; use constant SQL_CREATE_ALBUM_ITEM => q{ INSERT %s INTO fulltext (id, type, w10, w5, w3, w1) - SELECT albums.id, 'album', + SELECT albums.id, 'album', -- weight 10 - IFNULL(albums.title, '') || ' ' || IFNULL(albums.titlesearch, '') || ' ' || IFNULL(albums.customsearch, '') || ' ' || IFNULL(albums.musicbrainz_id, ''), + LOWER(IFNULL(albums.title, '')) || ' ' || IFNULL(albums.titlesearch, '') || ' ' || IFNULL(albums.customsearch, ''), -- weight 5 IFNULL(albums.year, ''), -- weight 3 CONCAT_CONTRIBUTOR_ROLE(albums.id, GROUP_CONCAT(contributor_album.contributor, ','), 'contributor_album'), -- weight 1 - CONCAT_ALBUM_TRACKS_INFO(albums.id) || ' ' || CASE WHEN albums.compilation THEN 'compilation' ELSE '' END - + CONCAT_ALBUM_TRACKS_INFO(albums.id) || ' ' || CASE WHEN albums.compilation THEN 'compilation' ELSE '' END || ' ' + || LOWER(IFNULL(albums.musicbrainz_id, '')) + FROM albums LEFT JOIN contributor_album ON contributor_album.album = albums.id LEFT JOIN contributors ON contributors.id = contributor_album.contributor - + %s - + GROUP BY albums.id; }; use constant SQL_CREATE_CONTRIBUTOR_ITEM => q{ INSERT %s INTO fulltext (id, type, w10, w5, w3, w1) - SELECT contributors.id, 'contributor', + SELECT contributors.id, 'contributor', -- weight 10 - IFNULL(contributors.name, '') || ' ' || IFNULL(contributors.namesearch, '') || ' ' || IFNULL(contributors.customsearch, '') || ' ' || IFNULL(contributors.musicbrainz_id, ''), + LOWER(IFNULL(contributors.name, '')) || ' ' || IFNULL(contributors.namesearch, '') || ' ' || IFNULL(contributors.customsearch, ''), -- weight 5 '', -- weight 3 '', -- weight 1 - '' + LOWER(IFNULL(contributors.musicbrainz_id, '')) FROM contributors %s; }; @@ -90,7 +98,7 @@ my $popularTerms; sub initPlugin { my $class = shift; - + return unless $class->canFulltextSearch; Slim::Music::Import->addImporter('Slim::Plugin::FullTextSearch::Plugin', { @@ -121,10 +129,10 @@ sub initPlugin { } # importer modules, run in the scanner -sub startScan { +sub startScan { my $class = shift; - my $progress = Slim::Utils::Progress->new({ + my $progress = Slim::Utils::Progress->new({ 'type' => 'importer', 'name' => 'plugin_fulltext', 'total' => BUILD_STEPS, # number of SQL queries - to be adjusted if there are more @@ -140,9 +148,9 @@ sub startScan { # this won't do any cleanup, might leave stale entries behind sub checkSingleTrack { my ( $trackObj, $url ) = @_; - + return if $trackObj->remote || !$trackObj->id; - + my $dbh = Slim::Schema->dbh; $dbh->do( sprintf(SQL_CREATE_TRACK_ITEM, 'OR REPLACE', 'WHERE tracks.id=?'), undef, $trackObj->id ); @@ -153,30 +161,30 @@ sub checkSingleTrack { sub canFulltextSearch { # we only support fulltext search with sqlite my $sqlVersion = Slim::Utils::OSDetect->getOS->sqlHelperClass->sqlVersion( Slim::Schema->dbh ); - + return 1 if $sqlVersion =~ /SQLite/i; - + $log->error("We don't support fulltext search on your SQL engine: $sqlVersion"); - + Slim::Utils::PluginManager->disablePlugin('FullTextSearch'); - + return 0; } sub createHelperTable { my ($class, $args) = @_; - + if (! ($args->{name} && defined $args->{search} && $args->{type}) ) { $log->error("Can't create helper table without a name and search terms"); return; } - + my $name = $args->{name}; my $type = $args->{type}; my ($tokens, $isLarge); my $orderOrLimit = ''; - + if ($args->{checkLargeResultset}) { ($tokens, $isLarge) = $class->parseSearchTerm($args->{search}, $type); $orderOrLimit = $args->{checkLargeResultset}->($isLarge); @@ -186,13 +194,13 @@ sub createHelperTable { } my $dbh = Slim::Schema->dbh; - + $dbh->do('DROP TABLE IF EXISTS ' . $name); - + my $temp = (main::DEBUGLOG && $log->is_debug) ? '' : 'TEMPORARY'; - + $orderOrLimit = 'LIMIT 0' if !$tokens; - + my $searchSQL = "CREATE $temp TABLE $name AS SELECT id, FULLTEXTWEIGHT(matchinfo(fulltext)) AS fulltextweight FROM fulltext WHERE fulltext MATCH 'type:$type $tokens' $orderOrLimit"; if ( main::DEBUGLOG ) { @@ -211,12 +219,14 @@ sub dropHelperTable { sub parseSearchTerm { my ($class, $search, $type) = @_; + $search = lc($search || ''); + # Check if we have an open double quote and close it if needed my $c = () = $search =~ /"/g; if ( $c % 2 == 1 ) { $search .= '"'; } - + $search =~ s/""\s*$//; # don't pull quoted strings apart! @@ -230,13 +240,13 @@ sub parseSearchTerm { my @tokens = grep /\w+/, split(/[\s[:punct:]]/, $search); my $noOfTokens = scalar(@tokens) + scalar(@quoted); - my @tokens = map { + my @tokens = map { my $token = "$_*"; # if this is the first token, then handle a few keywords which might result in a huge list carefully if ($noOfTokens == 1) { if ( length $_ == 1 ) { - $token = "w10:$_"; + $token = "w10:$_"; } elsif ( /\d{4}/ ) { # nothing to do here: years can be popular, but we want to be able to search for them @@ -246,7 +256,7 @@ sub parseSearchTerm { # only respect once there is eg. "artist:e*" elsif ( $_ !~ /a\w+:\w+/ && $popularTerms =~ /\Q$_\E[^|]*/i ) { $token = "w10:$_*"; - + # log warning about search for popular term (set flag in cache to only warn once) $ftsCache{uc($token)}++ || (main::DEBUGLOG && $log->is_debug && $log->debug("Searching for very popular term - limiting to highest weighted column to prevent huge result list: '$token'")); } @@ -258,25 +268,32 @@ sub parseSearchTerm { $token; } @tokens; - + + # conditionally revert single character optimization: if one of the tokens is more than one character, allow single characters with wildcard + if ( $noOfTokens && 1 && grep { index($_, '*') > 1 } @tokens ) { + @tokens = map { + length($_) == 1 ? "$_*" : $_; + } @tokens; + } + @quoted = map { my $token = $_; if ($noOfTokens == 1) { my ($raw) = $token =~ /"(.*)"/; - + if ( $popularTerms =~ /\Q$raw\E[^|]*/i ) { $token = "w10:$raw"; - + # log warning about search for popular term (set flag in cache to only warn once) $ftsCache{uc($token)}++ || (main::DEBUGLOG && $log->is_debug && $log->debug("Searching for very popular term - limiting to highest weighted column to prevent huge result list: '$token'")); } } - + $token; } @quoted; - + my $tokens = join(' AND ', @quoted, @tokens); - + # handle exclusions "paul simon -garfunkel" $tokens =~ s/ AND -/ NOT /g; @@ -284,10 +301,10 @@ sub parseSearchTerm { # make sure our custom functions are registered my $dbh = Slim::Schema->dbh; - + if (wantarray && $type && $tokens) { my $counts = $ftsCache{ uc($type . '|' . $tokens) }; - + if (!defined $counts) { ($counts) = $dbh->selectrow_array(sprintf("SELECT count(1) FROM fulltext WHERE fulltext MATCH 'type:%s %s'", $type, $tokens)); $ftsCache{ uc($type . '|' . $tokens) } = $counts; @@ -295,25 +312,25 @@ sub parseSearchTerm { $isLargeResultSet = LARGE_RESULTSET if $counts && $counts > LARGE_RESULTSET; } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug("Search token ($type): '$tokens'"); $log->debug("Large resultset? " . ($isLargeResultSet ? 'yes' : 'no')); }; - + return wantarray ? ($tokens, $isLargeResultSet) : $tokens; } # Calculate the record's weight: columns are weighed according to their importance # http://www.sqlite.org/fts3.html#matchinfo -# http://www.sqlite.org/fts3.html#fts4aux - get information about the index and tokens +# http://www.sqlite.org/fts3.html#fts4aux - get information about the index and tokens sub _getWeight { my $v = shift; - + my ($phraseCount, $columnCount) = unpack('LL', $v); - + my @x = unpack(('x' x 8) . ('L' x (3*$phraseCount*$columnCount)), $v); - + my $weight = 0; # start at second phrase, as the first is the type (track, album, contributor, playlist) for (my $i = 1; $i < $phraseCount; $i++) { @@ -322,24 +339,24 @@ sub _getWeight { + $x[3 * ((FIRST_COLUMN + 2) + $i * $columnCount)] * 3 # comments, lyrics + $x[3 * ((FIRST_COLUMN + 3) + $i * $columnCount)]; # bitrate sample size } - - return $weight; + + return $weight; } sub _getContributorRole { my ($workId, $contributors, $type) = @_; - + my ($col) = $type =~ /contributor_(.*)/; - + return '' unless $workId && $contributors && $type && $col; - + my $dbh = Slim::Schema->dbh; - my $sth = $dbh->prepare_cached("SELECT name, namesearch, role FROM contributors, $type WHERE contributors.id = ? AND $type.contributor = ? AND $type.$col = ? GROUP BY role"); + my $sth = $dbh->prepare_cached("SELECT LOWER(name), namesearch, role FROM contributors, $type WHERE contributors.id = ? AND $type.contributor = ? AND $type.$col = ? GROUP BY role"); my ($name, $namesearch, $role); - + my $tuples = ''; - + foreach my $contributor ( split /,/, $contributors ) { $sth->execute($contributor, $contributor, $workId); $sth->bind_columns(\$name, \$namesearch, \$role); @@ -349,7 +366,7 @@ sub _getContributorRole { my $localized = Slim::Utils::Strings::string($role); utf8::decode($name); - + $tuples .= "$role:$name $localized:$name " if $name; $tuples .= "$role:$namesearch $localized:$namesearch " if $namesearch; } @@ -360,22 +377,22 @@ sub _getContributorRole { sub _getAlbumTracksInfo { my ($albumId) = @_; - + return '' unless $albumId; - + my $dbh = Slim::Schema->dbh; # XXX - should we include artist information? my $sth = $dbh->prepare_cached(qq{ - SELECT IFNULL(tracks.title, '') || ' ' || IFNULL(tracks.titlesearch, '') || ' ' || IFNULL(tracks.customsearch, '') || ' ' || - IFNULL(tracks.musicbrainz_id, '') || ' ' || IGNORE_CASE(tracks.lyrics) || ' ' || IGNORE_CASE(comments.value) - FROM tracks + SELECT LOWER(IFNULL(tracks.title, '')) || ' ' || IFNULL(tracks.titlesearch, '') || ' ' || IFNULL(tracks.customsearch, '') || ' ' || + IFNULL(tracks.musicbrainz_id, '') || ' ' || IGNORE_CASE(tracks.lyrics) || ' ' || IGNORE_CASE(comments.value) + FROM tracks LEFT JOIN comments ON comments.track = tracks.id WHERE tracks.album = ? GROUP BY tracks.id; }); my $trackInfo = join(' ', @{ $dbh->selectcol_arrayref($sth, undef, $albumId) || [] }); - + $trackInfo =~ s/^ +//; $trackInfo =~ s/ +/ /; @@ -384,35 +401,42 @@ sub _getAlbumTracksInfo { sub _ignoreCase { my ($text) = @_; - + return '' unless $text; - + return $text . ' ' . Slim::Utils::Text::ignoreCase($text, 1); } sub _rebuildIndex { my $progress = shift; - + $scanlog->error("Starting fulltext index build"); my $dbh = Slim::Schema->dbh; $scanlog->error("Initialize fulltext table"); - + $dbh->do("DROP TABLE IF EXISTS fulltext;") or $scanlog->error($dbh->errstr); - $dbh->do("CREATE VIRTUAL TABLE fulltext USING fts3(id, type, w10, w5, w3, w1);") or $scanlog->error($dbh->errstr); + if ( CAN_FTS4 ) { + main::DEBUGLOG && $log->debug("New SQLite - enable advanced fts4 features (notindexed)"); + $dbh->do("CREATE VIRTUAL TABLE fulltext USING fts4(id, type, w10, w5, w3, w1, matchinfo=fts3, notindexed=id);") or $scanlog->error($dbh->errstr); + } + else { + main::DEBUGLOG && $log->debug("Old SQLite - don't enable advanced fts4 features"); + $dbh->do("CREATE VIRTUAL TABLE fulltext USING fts3(id, type, w10, w5, w3, w1);") or $scanlog->error($dbh->errstr); + } main::idleStreams() unless main::SCANNER; $scanlog->error("Create fulltext index for tracks"); $progress && $progress->update(string('SONGS')); Slim::Schema->forceCommit if main::SCANNER; - + my $sql = sprintf(SQL_CREATE_TRACK_ITEM, '', ''); # main::DEBUGLOG && $scanlog->is_debug && $scanlog->debug($sql); $dbh->do($sql) or $scanlog->error($dbh->errstr); main::idleStreams() unless main::SCANNER; - + $scanlog->error("Create fulltext index for albums"); $progress && $progress->update(string('ALBUMS')); Slim::Schema->forceCommit if main::SCANNER; @@ -421,7 +445,7 @@ sub _rebuildIndex { # main::DEBUGLOG && $scanlog->is_debug && $scanlog->debug($sql); $dbh->do($sql) or $scanlog->error($dbh->errstr); main::idleStreams() unless main::SCANNER; - + $scanlog->error("Create fulltext index for contributors"); $progress && $progress->update(string('ARTISTS')); Slim::Schema->forceCommit if main::SCANNER; @@ -437,8 +461,8 @@ sub _rebuildIndex { Slim::Schema->forceCommit if main::SCANNER; # building fulltext information for playlists is a bit more involved, as we want to have its tracks' information, too - my $plSql = "SELECT track FROM playlist_track WHERE playlist = ?"; - my $trSql = "SELECT w10 || ' ' || w5 || ' ' || w3 || ' ' || w1 FROM tracks,fulltext WHERE tracks.url = ? AND fulltext MATCH 'id:' || tracks.id || ' type:track'"; + my $plSql = "SELECT track FROM playlist_track WHERE playlist = ? AND track LIKE 'file:%'"; + my $trSql = "SELECT w10 || ' ' || w5 || ' ' || w3 || ' ' || w1 FROM tracks,fulltext WHERE tracks.url = ? AND fulltext.id = tracks.id AND fulltext.type = 'track'"; my $inSql = "INSERT INTO fulltext (id, type, w10, w5, w3, w1) VALUES (?, 'playlist', ?, '', '', ?)"; # use fulltext information for tracks to populate a playlist's record with track information @@ -448,18 +472,16 @@ sub _rebuildIndex { main::DEBUGLOG && $scanlog->is_debug && $scanlog->error( $plSql . ' [' . Data::Dump::dump($playlist->id) .']' ); my $w1 = ''; - - foreach my $track ( @{ $dbh->selectcol_arrayref($plSql, undef, $playlist->id) } ) { - next unless $track =~ /^file:/; + foreach my $track ( @{ $dbh->selectcol_arrayref($plSql, undef, $playlist->id) } ) { main::DEBUGLOG && $scanlog->is_debug && $scanlog->debug($trSql . ' - ' . $track); $w1 .= join(' ', @{ $dbh->selectcol_arrayref($trSql, undef, $track) }); } - + $w1 =~ s/^ +//; $w1 =~ s/ +/ /; - + main::DEBUGLOG && $scanlog->is_debug && $scanlog->debug( $inSql . Data::Dump::dump($playlist->id, $playlist->title . ' ' . $playlist->titlesearch, $w1) ); $dbh->do($inSql, undef, $playlist->id, $playlist->title . ' ' . $playlist->titlesearch, $w1) or $scanlog->error($dbh->errstr); @@ -478,7 +500,7 @@ sub _rebuildIndex { $dbh->do("DROP TABLE IF EXISTS fulltext_terms;") or $scanlog->error($dbh->errstr); $dbh->do("CREATE VIRTUAL TABLE fulltext_terms USING fts4aux(fulltext);") or $scanlog->error($dbh->errstr); - + $progress->final(BUILD_STEPS) if $progress; Slim::Schema->forceCommit if main::SCANNER; @@ -487,19 +509,19 @@ sub _rebuildIndex { sub _initPopularTerms { my $scanDone = shift; - + return if ($popularTerms = join('|', @{ $prefs->get('popularTerms') || [] })); main::DEBUGLOG && $log->is_debug && $log->debug("Analyzing most popular tokens"); - + my $dbh = Slim::Schema->dbh; - + my ($ftExists) = $dbh->selectrow_array( qq{ SELECT name FROM sqlite_master WHERE type='table' AND name='fulltext' } ); ($ftExists) = $dbh->selectrow_array( qq{ SELECT name FROM sqlite_master WHERE type='table' AND name='fulltext_terms' } ) if $ftExists; - + if (!$ftExists) { $scanlog->error("Fulltext index missing or outdated - re-building"); - + $prefs->remove('popularTerms'); _rebuildIndex() unless $scanDone; } @@ -507,10 +529,10 @@ sub _initPopularTerms { # get a list of terms which occur more than LARGE_RESULTSET times in our database my $terms = $dbh->selectcol_arrayref( sprintf(qq{ SELECT term, d FROM ( - SELECT term, SUM(documents) d - FROM fulltext_terms + SELECT term, SUM(documents) d + FROM fulltext_terms WHERE NOT col IN ('*', 1, 0) AND LENGTH(term) > 1 - GROUP BY term + GROUP BY term ORDER BY d DESC ) WHERE d > %i @@ -524,13 +546,13 @@ sub _initPopularTerms { sub postDBConnect { my ($class, $dbh) = @_; - + # some custom functions to get good data $dbh->sqlite_create_function( 'FULLTEXTWEIGHT', 1, \&_getWeight ); $dbh->sqlite_create_function( 'CONCAT_CONTRIBUTOR_ROLE', 3, \&_getContributorRole ); $dbh->sqlite_create_function( 'CONCAT_ALBUM_TRACKS_INFO', 1, \&_getAlbumTracksInfo ); $dbh->sqlite_create_function( 'IGNORE_CASE', 1, \&_ignoreCase); - + # XXX - printf is only available in SQLite 3.8.3+ $dbh->sqlite_create_function( 'printf', 2, sub { sprintf(shift, shift); } ); } diff --git a/Slim/Plugin/ImageBrowser/Plugin.pm b/Slim/Plugin/ImageBrowser/Plugin.pm index 66cddc9ab1f..c5c6a87ecef 100644 --- a/Slim/Plugin/ImageBrowser/Plugin.pm +++ b/Slim/Plugin/ImageBrowser/Plugin.pm @@ -1,8 +1,7 @@ package Slim::Plugin::ImageBrowser::Plugin; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/InfoBrowser/Plugin.pm b/Slim/Plugin/InfoBrowser/Plugin.pm index aca8337a205..9a9aa9547ad 100644 --- a/Slim/Plugin/InfoBrowser/Plugin.pm +++ b/Slim/Plugin/InfoBrowser/Plugin.pm @@ -2,7 +2,6 @@ package Slim::Plugin::InfoBrowser::Plugin; # InfoBrowser - an extensible information parser for Logitech Media Server 7.0 # -# $Id$ # # InfoBrowser provides a framework to use Squeezebox Server's xmlbrowser to fetch remote content and convert it into a format # which can be displayed via the server web interface, cli for jive or another cli client or via the player display. diff --git a/Slim/Plugin/InfoBrowser/strings.txt b/Slim/Plugin/InfoBrowser/strings.txt index 046aee4fef3..7e14428e0c2 100644 --- a/Slim/Plugin/InfoBrowser/strings.txt +++ b/Slim/Plugin/InfoBrowser/strings.txt @@ -24,7 +24,7 @@ PLUGIN_INFOBROWSER_DESC FI Nouda tietoa internet-lähteistä ja tarkastele sitä kaukosäätimen, palvelimen tai soittimen näytössä. Asetuksissa on lisätietoa lisäinformaatiolähteiden lisäämisestä. FR Récupère des informations d'Internet et les affiche sur la télécommande, sur le serveur ou sur la platine. Reportez-vous à la section des réglages pour en savoir plus sur l'ajout de sources d'informations. IT Recupera le informazioni da fonti Internet e le visualizza nel display del telecomando, del server o del lettore. Consultare la sezione relativa alle impostazioni per istruzioni sull'aggiunta di altre fonti di informazioni. - NL Haal informatie op uit internetbronnen en geef deze via de afstandsbediening, de server of het muzieksysteem weer. Zie de instellingensectie voor informatie over het toevoegen van extra informatiebronnen. + NL Haal informatie op uit internetbronnen en geef deze via de afstandsbediening, de server of de muziekspeler weer. Zie de instellingensectie voor informatie over het toevoegen van extra informatiebronnen. NO Hent informasjon fra Internett-kilder og på skjermene til serveren, fjernkontrollen og spilleren. Du finner mer informasjon om å legge til flere informasjonskilder under Innstillinger. PL Pobierz informacje ze źródeł internetowych i zobacz je na wyświetlaczu pilota, serwera lub odtwarzacza. Informacje na temat dodawania kolejnych źródeł informacji zawiera sekcja ustawień. RU Извлекайте информацию из Интернета и отображайте ее на экране плеера, сервера или пульта ДУ. Сведения о добавлении дополнительных источников информации см. в разделе "Настройки". @@ -39,7 +39,7 @@ SETUP_PLUGIN_INFOBROWSER_DESC FI Informaatioselaimen avulla voit lukea informaatiota internet-lähteistä Squeezebox-kaukosäätimen, -palvelimen tai -soittimen näytössä. Voit lisätä muita informaatiolähteitä internet-käyttöliittymässä, kohdassa Muut/Informaatioselain. FR Le Navigateur info vous permet de lire des flux d'informations en provenance d'Internet directement sur la télécommande, sur le serveur ou sur la platine. D'autres sources d'informations peuvent être ajoutées à l'aide de l'interface Web sous Extras/Navigateur info. IT Il browser informazioni consente di visualizzare le informazioni recuperate dalle fonti Internet nel telecomando di Squeezebox, nel server o nel display del lettore. È possibile aggiungere ulteriori fonti di informazioni tramite l'interfaccia Web in Extra/Browser informazioni. - NL Met behulp van de Infobrowser kun je informatie uit internetbronnen op je Squeezebox-afstandsbediening, -server of -muzieksysteemdisplay lezen. Je kunt meer informatiebronnen toevoegen via 'Extra's/Infobrowser' in de webinterface. + NL Met behulp van de Infobrowser kun je informatie uit internetbronnen op je Squeezebox-afstandsbediening, -server of -muziekspeler lezen. Je kunt meer informatiebronnen toevoegen via 'Extra's/Infobrowser' in de webinterface. NO Med informasjonsleseren kan du lese informasjon fra Internett-kilder på Squeezebox-fjernkontrollen, serveren eller spillerskjermen. Du kan legge til flere informasjonskilder ved å bruke webgrensesnittet under Tillegg/Informasjonsleser. PL Przeglądarka informacji umożliwia odczytanie informacji ze źródeł internetowych na wyświetlaczu pilota urządzenia Squeezebox, serwera lub odtwarzacza. Kolejne źródła informacji można dodać za pomocą interfejsu internetowego w obszarze Dodatki/Przeglądarka informacji. RU Информационный браузер позволяет просматривать информацию из Интернета на экране плеера, сервера или пульта ДУ Squeezebox. Веб-интерфейс позволяет добавлять дополнительные источники ("Дополнения/Информационный браузер"). diff --git a/Slim/Plugin/InternetRadio/Plugin.pm b/Slim/Plugin/InternetRadio/Plugin.pm index dee80d457fe..6e8151192e6 100644 --- a/Slim/Plugin/InternetRadio/Plugin.pm +++ b/Slim/Plugin/InternetRadio/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::InternetRadio::Plugin; -# $Id$ use strict; use base qw(Slim::Plugin::OPMLBased); @@ -10,7 +9,12 @@ use Slim::Plugin::InternetRadio::TuneIn; use Slim::Utils::Log; use Slim::Utils::Prefs; -my $log = logger('plugin.radio'); +my $log = Slim::Utils::Log->addLogCategory({ + 'category' => 'plugin.radio', + 'defaultLevel' => 'ERROR', + 'description' => 'RADIO', +}); + my $prefs = preferences('server'); sub initPlugin { diff --git a/Slim/Plugin/InternetRadio/TuneIn.pm b/Slim/Plugin/InternetRadio/TuneIn.pm index 022bd610400..1534db73208 100644 --- a/Slim/Plugin/InternetRadio/TuneIn.pm +++ b/Slim/Plugin/InternetRadio/TuneIn.pm @@ -1,6 +1,6 @@ package Slim::Plugin::InternetRadio::TuneIn; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/InternetRadio/TuneIn/Metadata.pm b/Slim/Plugin/InternetRadio/TuneIn/Metadata.pm index 6541d391630..7e6e31c853d 100644 --- a/Slim/Plugin/InternetRadio/TuneIn/Metadata.pm +++ b/Slim/Plugin/InternetRadio/TuneIn/Metadata.pm @@ -1,6 +1,6 @@ package Slim::Plugin::InternetRadio::TuneIn::Metadata; -# Logitech Media Server Copyright 2001-2013 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -21,6 +21,7 @@ use URI::Escape qw(uri_escape_utf8); my $log = logger('plugin.radio'); my $prefs = preferences('plugin.radiotime'); +my $cache = Slim::Utils::Cache->new(); use constant PARTNER_ID => 16; use constant META_URL => 'http://opml.radiotime.com/NowPlaying.aspx?partnerId=' . PARTNER_ID; @@ -29,31 +30,43 @@ use constant ICON => 'plugins/TuneIn/html/images/icon.png'; sub init { my $class = shift; - + Slim::Formats::RemoteMetadata->registerParser( match => qr/(?:radiotime|tunein)\.com/, func => \&parser, ); - + Slim::Formats::RemoteMetadata->registerProvider( match => qr/(?:radiotime|tunein)\.com/, func => \&provider, ); - + # match one of the following types of artwork: # http://xxx.cloudfront.net/293541660g.jpg # http://xxx.cloudfront.net/gn/6LN8BZKP0Mg.jpg + # http://cdn-profiles.tunein.com/s20291/images/logoq.png?t=1 + # http://cdn-radiotime-logos.tunein.com/s8295g.png Slim::Web::ImageProxy->registerHandler( match => qr/cloudfront\.net\/(?:[ps]?\d+|gn\/[A-Z0-9]+)[tqgd]?\.(?:jpe?g|png|gif)$/, func => \&artworkUrl, ); + + Slim::Web::ImageProxy->registerHandler( + match => qr/cdn-profiles\.tunein\.com\/.*\/logo[tqgd]\.(?:jpe?g|png|gif)/, + func => \&artworkUrl, + ); + + Slim::Web::ImageProxy->registerHandler( + match => qr/cdn-radiotime-logos\.tunein\.com\/s\d+[tqdg]\.(?:jpe?g|png|gif)/, + func => \&artworkUrl, + ); } sub getConfig { my $client = shift; - + Slim::Utils::Timers::killTimers( $client, \&getConfig ); - + my $http = Slim::Networking::SimpleAsyncHTTP->new( \&_gotConfig, \&_gotConfig, # TODO - error handler @@ -62,7 +75,7 @@ sub getConfig { timeout => 30, }, ); - + Slim::Utils::Timers::setTimer( $client, time() + 60*60*23, # repeat at least every 24h @@ -75,11 +88,11 @@ sub getConfig { sub _gotConfig { my $http = shift; my $client = $http->params('client'); - + my $feed = eval { Slim::Formats::XML::parseXMLIntoFeed( $http->contentRef ) }; if ( $@ ) { - main::DEBUGLOG && $log->debug( "Error fetching TuneIn artwork configuration: $@" ); + $log->warn( "Error fetching TuneIn artwork configuration: $@" ); } elsif ( $feed && $feed->{items} && (my $config = $feed->{items}->[0]) ) { if ( (my $lookup = $config->{'albumart.lookupurl'}) && (my $url = $config->{'albumart.url'}) ) { @@ -93,7 +106,7 @@ sub _gotConfig { sub defaultMeta { my ( $client, $url ) = @_; - + return { title => Slim::Music::Info::getCurrentTitle($url), icon => ICON, @@ -104,9 +117,9 @@ sub defaultMeta { sub parser { my ( $client, $url, $metadata ) = @_; - + $client = $client->master if $client; - + if ( $client && !$client->pluginData('artworkConfig') ) { getConfig($client); } @@ -117,7 +130,7 @@ sub parser { if ( $1 ) { if ( $client->pluginData('metadata' ) ) { main::DEBUGLOG && $log->is_debug && $log->debug('Disabling TuneIn metadata, stream has Icy metadata'); - + Slim::Utils::Timers::killTimers( $client, \&fetchMetadata ); $client->pluginData( metadata => undef ); } @@ -133,98 +146,102 @@ sub parser { # lookup artwork unless it's been defined in the metadata (eg. Radio Paradise) fetchArtwork($client, $url, 'delayed') unless $artworkUrl; - + # Let the default metadata handler process the Icy metadata $client->pluginData( hasIcy => $url ); return; } } - + # If a station is providing WMA metadata, disable metadata # provided by TuneIn elsif ( $metadata =~ /(?:CAPTION|artist|type=SONG)/ ) { if ( $client->pluginData('metadata' ) ) { main::DEBUGLOG && $log->is_debug && $log->debug('Disabling TuneIn metadata, stream has WMA metadata'); - + Slim::Utils::Timers::killTimers( $client, \&fetchMetadata ); $client->pluginData( metadata => undef ); } fetchArtwork($client, $url, 'delayed'); - + # Let the default metadata handler process the WMA metadata $client->pluginData( hasIcy => $url ); return; } - + return 1; } sub provider { my ( $client, $url ) = @_; - + return defaultMeta(undef, $url) unless $client; - + $client = $client->master; - + my $hasIcy = $client->pluginData('hasIcy'); - + if ( $hasIcy && $hasIcy ne $url ) { $client->pluginData( hasIcy => 0 ); $hasIcy = undef; } - + return {} if $hasIcy; - + if ( !$client->isPlaying && !$client->isPaused ) { return defaultMeta( $client, $url ); } - + if ( my $meta = $client->pluginData('metadata') ) { if ( $meta->{_url} eq $url ) { if ( !$meta->{title} ) { $meta->{title} = Slim::Music::Info::getCurrentTitle($url); } - + return $meta; } } - - if ( !$client->pluginData('fetchingMeta') ) { + + # Sometimes when a slimservice instances on MySB/UESR is stopped, we might end up + # with fetchingMeta not being reset. As pluginData is persisted in the database, + # this would cause a player to never display artwork again. Let's therefore add a + # timestamp rather than a simple flag, and ignore the timestamp, when it's old. + if ( !$client->pluginData('fetchingMeta') || $client->pluginData('fetchingMeta') < (time() - 3600) ) { # Fetch metadata in the background Slim::Utils::Timers::killTimers( $client, \&fetchMetadata ); fetchMetadata( $client, $url ); } - + return defaultMeta( $client, $url ); } sub fetchMetadata { my ( $client, $url ) = @_; - + return unless $client; - + $client = $client->master; - + # Make sure client is still playing this station if ( Slim::Player::Playlist::url($client) ne $url ) { main::DEBUGLOG && $log->is_debug && $log->debug( $client->id . " no longer playing $url, stopping metadata fetch" ); return; } - + my ($stationId) = $url =~ m/(?:station)?id=([^&]+)/i; # support old-style stationId= and new id= URLs return unless $stationId; - + my $username = Slim::Plugin::InternetRadio::TuneIn->getUsername($client); - + my $metaUrl = META_URL . '&id=' . $stationId; - + if ( $username ) { $metaUrl .= '&username=' . uri_escape_utf8($username); } - + main::DEBUGLOG && $log->is_debug && $log->debug( "Fetching TuneIn metadata from $metaUrl" ); - + my $http = Slim::Networking::SimpleAsyncHTTP->new( \&_gotMetadata, \&_gotMetadataError, @@ -234,9 +251,16 @@ sub fetchMetadata { timeout => 30, }, ); - - $client->pluginData( fetchingMeta => 1 ); - + + $client->pluginData( fetchingMeta => time() ); + + # keep track of the station logo as found in the browse OPML data + if (my $logo = $cache->get("remote_image_$url") || $cache->get("station_logo_$stationId")) { + $client->pluginData( stationLogo => $logo ); + # make sure we keep this around long enough... + $cache->set("station_logo_$stationId", $logo, '30 days'); + } + $http->get( $metaUrl ); } @@ -244,64 +268,71 @@ sub _gotMetadata { my $http = shift; my $client = $http->params('client'); my $url = $http->params('url'); - + my $feed = eval { Slim::Formats::XML::parseXMLIntoFeed( $http->contentRef ) }; - + if ( $@ ) { $http->error( $@ ); _gotMetadataError( $http ); return; } - + $client = $client->master; $client->pluginData( fetchingMeta => 0 ); - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Raw TuneIn metadata: " . Data::Dump::dump($feed) ); } - + my $ttl = 300; - + if ( my $cc = $http->headers->header('Cache-Control') ) { if ( $cc =~ m/max-age=(\d+)/i ) { $ttl = $1; } } - + + # enforce an update every few minutes + $ttl = 300 if $ttl > 300; + my $meta = defaultMeta( $client, $url ); $meta->{_url} = $url; - + my $i = 0; for my $item ( @{ $feed->{items} } ) { if ( $item->{image} ) { $meta->{cover} = $item->{image}; } - + if ( $i == 0 ) { $meta->{artist} = $item->{name}; } elsif ( $i == 1 ) { $meta->{title} = $item->{name}; } - + $i++; } - + + if ( (!$meta->{cover} || $meta->{cover} =~ /_0[tqgd]?\.(?:png|jp.?g)/) && (my $stationLogo = $client->pluginData('stationLogo')) ) { + $meta->{cover} = $stationLogo; + } + # Also cache the image URL in case the stream has other metadata if ( $meta->{cover} ) { setArtwork($client, $url, $meta->{cover}); } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Saved TuneIn metadata: " . Data::Dump::dump($meta) ); } - + $client->pluginData( metadata => $meta ); - + fetchArtwork($client, $url); - + main::DEBUGLOG && $log->is_debug && $log->debug( "Will check metadata again in $ttl seconds" ); - + Slim::Utils::Timers::setTimer( $client, time() + $ttl, @@ -315,28 +346,28 @@ sub _gotMetadataError { my $client = $http->params('client'); my $url = $http->params('url'); my $error = $http->error; - + main::DEBUGLOG && $log->is_debug && $log->debug( "Error fetching TuneIn metadata: $error" ); - + $client = $client->master; $client->pluginData( fetchingMeta => 0 ); - + # To avoid flooding the RT servers in the case of errors, we just ignore further # metadata for this station if we get an error my $meta = defaultMeta( $client, $url ); $meta->{_url} = $url; - + $client->pluginData( metadata => $meta ); } sub fetchArtwork { my ($client, $url, $delayed) = @_; - + $client = $client->master if $client; - + main::DEBUGLOG && $log->debug( "Getting artwork for $url" ); - + Slim::Utils::Timers::killTimers( $client, \&_fetchArtwork ); if ($delayed) { @@ -357,24 +388,38 @@ sub fetchArtwork { sub _fetchArtwork { my ( $client, $url ) = @_; - + $client = $client->master; - - my $config = $client->pluginData('artworkConfig') || return; + + my $config = $client->pluginData('artworkConfig'); + + if (!$config) { + if ( my $artworkUrl = $client->pluginData('stationLogo') ) { + main::DEBUGLOG && $log->debug("Falling back to station artwork lack of artwork lookup configuration"); + setArtwork($client, $url, $artworkUrl); + } + return; + } + my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url); if ( $handler && $handler->can('getMetadataFor') ) { my $track = $handler->getMetadataFor( $client, $url ); main::DEBUGLOG && $log->is_debug && $log->debug( 'Getting TuneIn artwork based on metadata:', Data::Dump::dump($track) ); - + # keep track of the station logo in case we don't get track artwork # [ps] => podcast or station # t => Thumbnail # q => sQuare # g => Giant # d => meDium - if ( $track->{cover} && $track->{cover} =~ m{/[ps]\d+[tqgd]?\.(?:jpg|jpeg|png|gif)$}i && (my $song = $client->playingSong()) ) { + if ( $track->{cover} && ($track->{cover} =~ m{/[ps]\d+[tqgd]?\.(?:jpg|jpeg|png|gif)$}i || $track->{cover} =~ /(_0[tqgd]?\.(?:png|jpg))/) && (my $song = $client->playingSong()) ) { + if ( $1 && (my $stationLogo = $client->pluginData('stationLogo')) ) { + main::DEBUGLOG && $log->debug( 'Storing default station artwork: ' . $stationLogo ); + $song->pluginData( stationLogo => $stationLogo ); + } + if ( !$song->pluginData('stationLogo') ) { main::DEBUGLOG && $log->debug( 'Storing default station artwork: ' . $track->{cover} ); @@ -382,20 +427,20 @@ sub _fetchArtwork { $client->pluginData( stationLogo => $track->{cover} ); } } - + if ( $track && $track->{title} && $track->{artist} ) { - + my $lookupurl = sprintf($config->{lookupurl} . '?partnerId=%s&serial=%s&artist=%s&title=%s', PARTNER_ID, Slim::Plugin::InternetRadio::TuneIn->getSerial($client), $track->{artist}, $track->{title}, ); - + return if $client->pluginData('fetchingArtwork') && $client->pluginData('fetchingArtwork') eq $lookupurl; - + $client->pluginData( fetchingArtwork => $lookupurl ); - + my $http = Slim::Networking::SimpleAsyncHTTP->new( \&_gotArtwork, \&_gotArtwork, # we'll happily fall back to the station artwork if we fail @@ -405,11 +450,12 @@ sub _fetchArtwork { timeout => 30, }, ); - + $http->get( $lookupurl ); } # fallback to station artwork elsif ( my $artworkUrl = $client->pluginData('stationLogo') ) { + main::DEBUGLOG && $log->debug("Falling back to station artwork lack of metadata"); setArtwork($client, $url, $artworkUrl); } } @@ -419,25 +465,25 @@ sub _gotArtwork { my $http = shift; my $client = $http->params('client'); my $url = $http->params('url'); - + $client = $client->master; $client->pluginData( fetchingArtwork => 0 ); my $feed = eval { Slim::Formats::XML::parseXMLIntoFeed( $http->contentRef ) }; - + if ( $@ || !$feed ) { main::DEBUGLOG && $log->debug( "Error fetching TuneIn artwork: $@" ); } else { main::DEBUGLOG && $log->is_debug && $log->debug( 'Received TuneIn track artwork information: ', Data::Dump::dump($feed) ); } - + if ( $feed && $feed->{items} && $feed->{items}->[0] && (my $key = $feed->{items}->[0]->{album_art} || $feed->{items}->[0]->{artist_art}) ) { my $config = $client->pluginData('artworkConfig'); # grab "g"iant artwork my $artworkUrl = $config->{albumarturl} . $key . 'g.jpg'; - + setArtwork($client, $url, $artworkUrl); } # fallback to station artwork @@ -448,12 +494,11 @@ sub _gotArtwork { sub setArtwork { my ($client, $url, $artworkUrl) = @_; - + $client = $client->master if $client; - - my $cache = Slim::Utils::Cache->new(); + $cache->set( "remote_image_$url", $artworkUrl, 3600 ); - + if ( my $song = $client->playingSong() ) { $song->pluginData( httpCover => $artworkUrl ); @@ -479,22 +524,29 @@ my $sizeMap = { # it uses the plugin's knowledge about available file sizes to optimize bandwidth and processing requirements sub artworkUrl { my ($url, $spec) = @_; - + main::DEBUGLOG && $log->debug("TuneIn artwork - let's get the smallest version fitting our needs: $url, $spec"); - + + # shortcut for station logo + if ( $url =~ s/(\/images\/logo)(?:[tgqd])/$1g/ + || $url =~ s/(cdn-radiotime-logos\.tunein\.com\/s\d+)[tqdg](\.png)/$1g$2/ ) { + main::DEBUGLOG && $log->debug("Going to get $url"); + return $url; + } + my ($logo, $id, $size) = $url =~ m{/([ps]?)(\d+)([tqgd]?)\.(jpg|jpeg|png|gif)$}i; $size = lc($size || ''); - + # sometimes the sQuare image differs from the others for _logos_ # don't use the larger, non-square in this case, otherwide default to largest $size = 'g' unless $logo && $size; my $ext = (Slim::Web::Graphics->parseSpec($spec))[4]; - + my $min = Slim::Web::ImageProxy->getRightSize($spec, $sizeMap); # we use either the min required, or the maximum as defined above - foreach (sort keys %$sizeMap) { + foreach (sort { $a <=> $b } keys %$sizeMap) { if ($sizeMap->{$_} eq $min) { $size = $min; last; @@ -504,9 +556,9 @@ sub artworkUrl { } $url =~ s/[tqgd]?\.$ext$/$size.$ext/ if $size; - + main::DEBUGLOG && $log->debug("Going to get $url"); - + return $url; } diff --git a/Slim/Plugin/InternetRadio/TuneIn/Settings.pm b/Slim/Plugin/InternetRadio/TuneIn/Settings.pm index 3bc0f8080c0..d261c4e1799 100644 --- a/Slim/Plugin/InternetRadio/TuneIn/Settings.pm +++ b/Slim/Plugin/InternetRadio/TuneIn/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::InternetRadio::TuneIn::Settings; -# Logitech Media Server Copyright 2001-2013 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/InternetRadio/install.xml b/Slim/Plugin/InternetRadio/install.xml index c33ca47ab32..61903514039 100644 --- a/Slim/Plugin/InternetRadio/install.xml +++ b/Slim/Plugin/InternetRadio/install.xml @@ -8,7 +8,6 @@ PLUGIN_INTERNET_RADIO_MODULE_NAME_DESC Logitech enabled - 1 http://www.mysqueezebox.com/support 2 diff --git a/Slim/Plugin/LMA/Plugin.pm b/Slim/Plugin/LMA/Plugin.pm index 7ab9fbf17bf..c07f01a37bf 100644 --- a/Slim/Plugin/LMA/Plugin.pm +++ b/Slim/Plugin/LMA/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::LMA::Plugin; -# $Id$ # Load Live Music Archive data via an OPML file - so we can ride on top of the Podcast Browser @@ -13,7 +12,7 @@ sub initPlugin { my $class = shift; Slim::Player::ProtocolHandlers->registerIconHandler( - qr/(?:archive\.org|squeezenetwork\.com.*\/lma\/)/, + qr/(?:archive\.org|mysqueezebox\.com.*\/lma\/)/, sub { return $class->_pluginDataFor('icon'); } ); diff --git a/Slim/Plugin/LMA/strings.txt b/Slim/Plugin/LMA/strings.txt index 33e4d8c737d..a6be4957012 100644 --- a/Slim/Plugin/LMA/strings.txt +++ b/Slim/Plugin/LMA/strings.txt @@ -1,7 +1,5 @@ # String file for LMA plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_LMA_MODULE_NAME CS Hudební archív Live DA Live Music Archive diff --git a/Slim/Plugin/LibraryDemo/Plugin.pm b/Slim/Plugin/LibraryDemo/Plugin.pm index a78294949ae..27f2d438fe4 100644 --- a/Slim/Plugin/LibraryDemo/Plugin.pm +++ b/Slim/Plugin/LibraryDemo/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::LibraryDemo::Plugin; -# Logitech Media Server Copyright 2001-2014 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/LineIn/Plugin.pm b/Slim/Plugin/LineIn/Plugin.pm index cb8a453ad3a..1a5ac8e8090 100644 --- a/Slim/Plugin/LineIn/Plugin.pm +++ b/Slim/Plugin/LineIn/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::LineIn::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/LineIn/ProtocolHandler.pm b/Slim/Plugin/LineIn/ProtocolHandler.pm index c6060ff2217..4d7d0725efc 100644 --- a/Slim/Plugin/LineIn/ProtocolHandler.pm +++ b/Slim/Plugin/LineIn/ProtocolHandler.pm @@ -1,8 +1,6 @@ package Slim::Plugin::LineIn::ProtocolHandler; -# $Id - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/LineIn/strings.txt b/Slim/Plugin/LineIn/strings.txt index 67c59ff41a7..4b27b7ce2bf 100644 --- a/Slim/Plugin/LineIn/strings.txt +++ b/Slim/Plugin/LineIn/strings.txt @@ -1,7 +1,5 @@ # String file for LineIn plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_LINE_IN CS Vstupní linka DA Linjeindgang @@ -26,7 +24,7 @@ PLUGIN_LINE_IN_DESC FI Boomin linjatulo FR Entrée ligne pour Boom IT Ingresso Boom - NL Lijningang voor boom + NL Lijningang voor Boom NO Lydinngang for Boom PL Wejście liniowe niskich tonów RU Линейный вход для Squeezebox Boom diff --git a/Slim/Plugin/LineOut/Plugin.pm b/Slim/Plugin/LineOut/Plugin.pm index 14a3b169d29..da94d741aff 100644 --- a/Slim/Plugin/LineOut/Plugin.pm +++ b/Slim/Plugin/LineOut/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::LineOut::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/LineOut/strings.txt b/Slim/Plugin/LineOut/strings.txt index 2a5ffd6983a..3a96356150c 100644 --- a/Slim/Plugin/LineOut/strings.txt +++ b/Slim/Plugin/LineOut/strings.txt @@ -1,7 +1,5 @@ # String file for Line Out plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_LINE_OUT CS Výstup pro zesilovač (Line Out) DA Linjeudgang diff --git a/Slim/Plugin/Live365/HTML/EN/plugins/Live365/html/images/icon.png b/Slim/Plugin/Live365/HTML/EN/plugins/Live365/html/images/icon.png deleted file mode 100755 index 451addd5865..00000000000 Binary files a/Slim/Plugin/Live365/HTML/EN/plugins/Live365/html/images/icon.png and /dev/null differ diff --git a/Slim/Plugin/Live365/HTML/EN/plugins/Live365/html/images/icon_40x40_m.png b/Slim/Plugin/Live365/HTML/EN/plugins/Live365/html/images/icon_40x40_m.png deleted file mode 100644 index 4be2246132c..00000000000 Binary files a/Slim/Plugin/Live365/HTML/EN/plugins/Live365/html/images/icon_40x40_m.png and /dev/null differ diff --git a/Slim/Plugin/Live365/Plugin.pm b/Slim/Plugin/Live365/Plugin.pm deleted file mode 100644 index 6abc805c612..00000000000 --- a/Slim/Plugin/Live365/Plugin.pm +++ /dev/null @@ -1,46 +0,0 @@ -package Slim::Plugin::Live365::Plugin; - -# $Id$ - -# Browse Live365 via SqueezeNetwork - -use strict; -use base qw(Slim::Plugin::OPMLBased); - -use Slim::Networking::SqueezeNetwork; -use Slim::Player::ProtocolHandlers; -use Slim::Plugin::Live365::ProtocolHandler; - -my $log = Slim::Utils::Log->addLogCategory( { - category => 'plugin.live365', - defaultLevel => 'ERROR', - description => 'PLUGIN_LIVE365_MODULE_NAME', -} ); - -sub initPlugin { - my $class = shift; - - Slim::Player::ProtocolHandlers->registerHandler( - live365 => 'Slim::Plugin::Live365::ProtocolHandler' - ); - - $class->SUPER::initPlugin( - feed => Slim::Networking::SqueezeNetwork->url('/api/live365/v1/opml'), - tag => 'live365', - is_app => 1, - ); - - Slim::Player::ProtocolHandlers->registerIconHandler( - qr/squeezenetwork\.com.*\/live365\//, - sub { return $class->_pluginDataFor('icon'); } - ); -} - -sub getDisplayName { - return 'PLUGIN_LIVE365_MODULE_NAME'; -} - -# Don't add this item to any menu -sub playerMenu { } - -1; diff --git a/Slim/Plugin/Live365/ProtocolHandler.pm b/Slim/Plugin/Live365/ProtocolHandler.pm deleted file mode 100644 index bb460276af9..00000000000 --- a/Slim/Plugin/Live365/ProtocolHandler.pm +++ /dev/null @@ -1,279 +0,0 @@ -package Slim::Plugin::Live365::ProtocolHandler; - -# $Id$ - -use strict; -use base qw( Slim::Player::Protocols::HTTP ); - -use JSON::XS::VersionOneAndTwo; -use URI::Escape qw(uri_escape); - -use Slim::Player::Playlist; -use Slim::Player::Source; -use Slim::Utils::Log; -use Slim::Utils::Misc; -use Slim::Utils::Timers; - -my $log = logger('plugin.live365'); - -sub new { - my $class = shift; - my $args = shift; - - my $url = $args->{url}; - my $client = $args->{client}; - my $song = $args->{'song'}; - my $self; - - my $realURL = $args->{'song'}->streamUrl(); - - if ( $url =~ m{^live365://} && $realURL ) { - - main::INFOLOG && $log->info("Requested: $url, streaming real URL $realURL"); - - $self = $class->SUPER::new( { - url => $realURL, - song => $song, - client => $client, - infoUrl => $url, - create => 1, - } ); - - Slim::Utils::Timers::killTimers( $song, \&getPlaylist ); - - Slim::Utils::Timers::setTimer( - $song, - Time::HiRes::time(), - \&getPlaylist, - ); - - } - else { - if ( $log->is_error ) { - $log->error( $client->string('PLUGIN_LIVE365_NO_URL') ); - } - } - - return $self; -} - -sub getFormatForURL () { 'mp3' } - -# Source for AudioScrobbler (R = Radio) -sub audioScrobblerSource () { 'R' } - -sub isRemote { 1 } - -sub gotURL { - my $http = shift; - my $params = $http->params; - my $song = $params->{'song'}; - - my $info = eval { from_json( $http->content ) }; - if ( $@ || $info->{error} ) { - $http->error( $@ || $info->{error} ); - return gotURLError( $http ); - } - - main::DEBUGLOG && $log->debug( "Got Live365 URL from SN: " . $info->{url} ); - - $song->streamUrl($info->{url}); - - $params->{callback}->(); -} - -sub gotURLError { - my $http = shift; - - if ( $log->is_error ) { - $log->error( "Error getting Live365 URL: " . $http->error ); - } - - $http->params->{errorCallback}->('PLUGIN_LIVE365_ERROR', $http->error); -} - -sub scanUrl { - my ($class, $url, $args) = @_; - $args->{'cb'}->($args->{'song'}->currentTrack()); -} - -sub getNextTrack { - my ($class, $song, $successCb, $errorCb) = @_; - - my $nextURL = $song->currentTrack()->url; - # Remove any existing session id - $nextURL =~ s/\?+//; - - # Talk to SN and get the channel info for this station - my $getAudioURL = Slim::Networking::SqueezeNetwork->url( - '/api/live365/v1/playback/getAudioURL?url=' . uri_escape($nextURL) - ); - - my $http = Slim::Networking::SqueezeNetwork->new( - \&gotURL, - \&gotURLError, - { - client => $song->master(), - url => $nextURL, - song => $song, - callback => $successCb, - errorCallback => $errorCb, - }, - ); - - main::DEBUGLOG && $log->debug( "Getting audio URL for $nextURL from SN" ); - - $http->get( $getAudioURL ); -} - -sub canDirectStreamSong { - my ( $class, $client, $song ) = @_; - - my $streamUrl = $song->streamUrl() || return undef; - - # We need to check with the base class (HTTP) to see if we - # are synced or if the user has set mp3StreamingMethod - $class->SUPER::canDirectStream( $client, $streamUrl ) || return undef; - - Slim::Utils::Timers::killTimers( $song, \&getPlaylist ); - - Slim::Utils::Timers::setTimer( - $song, - Time::HiRes::time(), - \&getPlaylist, - ); - - return $streamUrl; -} - -sub getPlaylist { - my ( $song ) = @_; - - my $client = $song->master(); - my $url = $song->currentTrack()->url; - - if ( $song != $client->streamingSong() || $client->isStopped() || $client->isPaused() ) { - main::DEBUGLOG && $log->debug( "Track changed, stopping playlist fetch" ); - return; - } - - # Talk to SN and get the playlist info - my ($station) = $url =~ m{play/([^/?]+)}; - - my $playlistURL = Slim::Networking::SqueezeNetwork->url( - '/api/live365/v1/playlist/' . uri_escape($station), - ); - - my $http = Slim::Networking::SqueezeNetwork->new( - \&gotPlaylist, - \&gotPlaylistError, - { - client => $client, - url => $url, - song => $song, - }, - ); - - main::DEBUGLOG && $log->debug("Getting playlist from SqueezeNetwork"); - - $http->get( $playlistURL ); -} - -sub gotPlaylist { - my $http = shift; - my $client = $http->params->{client}; - my $url = $http->params->{url}; - my $song = $http->params->{'song'}; - - my $track = eval { from_json( $http->content ) }; - - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( "Got current track: " . Data::Dump::dump($track) ); - } - - if ( $@ || $track->{error} ) { - $log->error( "Error getting current track: " . ( $@ || $track->{error} ) ); - - # Display the station name - my $title = Slim::Music::Info::title($url); - Slim::Music::Info::setCurrentTitle( $url, $title ); - - return; - } - - $song->pluginData($track); - - my $newTitle = $track->{title}; - - if ( !ref $track->{artist} ) { - $newTitle .= ' ' . $client->string('BY') . ' ' . $track->{artist}; - } - - if ( !ref $track->{album} ) { - $newTitle .= ' ' . $client->string('FROM') . ' ' . $track->{album}; - } - - if ( $newTitle eq 'NONE' ) { - # No title means it's an ad, use the desc field - $newTitle = $track->{desc}; - } - - # Delay the title set depending on buffered data - Slim::Music::Info::setDelayedTitle( $client, $url, $newTitle ); - - Slim::Utils::Timers::setTimer( - $song, - Time::HiRes::time() + $track->{refresh}, - \&getPlaylist, - ); -} - -sub gotPlaylistError { - my $http = shift; - my $error = $http->error; - - my $url = $http->params->{url}; - my $song = $http->params->{'song'}; - - $log->error( "Error getting current track: $error, will retry in 30 seconds" ); - - $song->pluginData(undef); - - # Display the station name - my $title = Slim::Music::Info::title($url); - Slim::Music::Info::setCurrentTitle( $url, $title ); - - # Try again - Slim::Utils::Timers::setTimer( - $song, - Time::HiRes::time() + 30, - \&getPlaylist, - ); -} - -sub getMetadataFor { - my ( $class, $client, $url, $forceCurrent ) = @_; - - my $song = $client->currentSongForUrl($url); - my $track = $song->pluginData() if $song; - - my $icon = $class->getIcon(); - - return { - artist => ( $track && !ref $track->{artist} ? $track->{artist} : undef ), - album => ( $track && !ref $track->{album} ? $track->{album} : undef ), - title => ( $track ) ? ( $track->{title} || $track->{desc} ) : undef, - cover => $icon, - icon => $icon, - type => 'MP3 (Live365)', - }; - -} - -sub getIcon { - my ( $class, $url ) = @_; - - return Slim::Plugin::Live365::Plugin->_pluginDataFor('icon'); -} - -1; diff --git a/Slim/Plugin/Live365/install.xml b/Slim/Plugin/Live365/install.xml deleted file mode 100644 index 2014029a6c9..00000000000 --- a/Slim/Plugin/Live365/install.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - 1108FB7A-9EE2-4750-89DB-1454E7E9C4D2 - PLUGIN_LIVE365_MODULE_NAME - Slim::Plugin::Live365::Plugin - 2.1 - PLUGIN_LIVE365_MODULE_NAME_DESC - Logitech - enabled - true - plugins/Live365/html/images/icon.png - http://www.mysqueezebox.com/appgallery/Live365 - 2 - - Logitech Media Server - 7.0a - * - - diff --git a/Slim/Plugin/Live365/strings.txt b/Slim/Plugin/Live365/strings.txt deleted file mode 100644 index b6d9d26d220..00000000000 --- a/Slim/Plugin/Live365/strings.txt +++ /dev/null @@ -1,64 +0,0 @@ -# String file for Live365 plugin - -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - -PLUGIN_LIVE365_MODULE_NAME - CS Live365 - DA Live365 - DE Live365 - EN Live365 - ES Live365 - FI Live365 - FR Live365 - IT Live365 - NL Live365 - NO Live365 - PL Live365 - RU Live365 - SV Live365 - -PLUGIN_LIVE365_MODULE_NAME_DESC - CS Přístup ke stanicím Live365 Internet Radio - DA Adgang til Live365-internetradiostationer - DE Zugriff auf Live365 Internet Radio - EN Access to Live365 Internet Radio stations - ES Acceso a emisoras de radio por Internet Live365 - FI Yhteys Live365-internet-radiokanaviin - FR Accès aux stations de radio Internet Live365 - IT Accesso alle stazioni radio Internet Live365 - NL Toegang tot Live365-internetradiostations - NO Tilgang til stasjonene fra Live365 Internet Radio - PL Dostęp do internetowych stacji radiowych usługi Live365 - RU Доступ к радиостанциям Интернета Live365 - SV Tillgång till Live365-webbradiostationer - -PLUGIN_LIVE365_ERROR - CS Chyba Live365 - DA Live365-fejl - DE Live365-Fehler - EN Live365 Error - ES ERROR de Live365 - FI Live365-virhe - FR Erreur Live365 - IT Errore di Live365 - NL Live365-foutmelding - NO Live365-feil - PL Błąd usługi Live365 - RU Ошибка Live365 - SV Live365-fel - -PLUGIN_LIVE365_NO_URL - CS Není možné načíst audio URL. K opětovnému pokusu stiskněte znovu PLAY. - DA Adressen kunne ikke hentes. Tryk på Afspil, og prøv igen. - DE Audio-URL konnte nicht geladen werden. Drücken Sie PLAY. - EN Unable to retrieve audio URL. Press PLAY to try again. - ES Imposible obtener URL de audio. Pulse PLAY para volver a intentarlo. - FI Ääni-URL:n noutaminen ei onnistu. Yritä uudelleen painamalla toistonäppäintä. - FR Impossible d'obtenir l'URL audio, appuyez sur PLAY pour réessayer. - IT Impossibile recuperare URL audio. Premere PLAY per riprovare. - NL Kan audio-URL niet ophalen. Druk op PLAY om het opnieuw te proberen. - NO Kan ikke hente adresse for lyd. Trykk på PLAY for å prøve på nytt. - PL Nie można pobrać adresu URL źródła dźwięku. Naciśnij przycisk PLAY, aby spróbować ponownie. - RU Невозможно извлечь URL аудио. Нажмите PLAY, чтобы повторить попытку. - SV Det går inte att hämta ljudwebbadressen. Tryck på Play om du vill försöka igen. - diff --git a/Slim/Plugin/MOG/Plugin.pm b/Slim/Plugin/MOG/Plugin.pm index a98c4416b03..e5c75367dc4 100644 --- a/Slim/Plugin/MOG/Plugin.pm +++ b/Slim/Plugin/MOG/Plugin.pm @@ -1,6 +1,9 @@ package Slim::Plugin::MOG::Plugin; -# $Id: Plugin.pm 10553 2011-08-03 15:29:58Z Shaahul $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; use base qw(Slim::Plugin::OPMLBased); @@ -25,7 +28,7 @@ sub initPlugin { ); Slim::Player::ProtocolHandlers->registerIconHandler( - qr|squeezenetwork\.com.*/api/mog/|, + qr|mysqueezebox\.com.*/api/mog/|, sub { $class->_pluginDataFor('icon') } ); diff --git a/Slim/Plugin/MOG/ProtocolHandler.pm b/Slim/Plugin/MOG/ProtocolHandler.pm index 686cdbf8896..bbc4fa30ab1 100644 --- a/Slim/Plugin/MOG/ProtocolHandler.pm +++ b/Slim/Plugin/MOG/ProtocolHandler.pm @@ -1,6 +1,9 @@ package Slim::Plugin::MOG::ProtocolHandler; -# $Id: ProtocolHandler.pm 31715 2011-08-16 13:14:29Z shameed $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; use base qw(Slim::Player::Protocols::HTTP); diff --git a/Slim/Plugin/MOG/install.xml b/Slim/Plugin/MOG/install.xml index dd64f95a744..569c36585d3 100644 --- a/Slim/Plugin/MOG/install.xml +++ b/Slim/Plugin/MOG/install.xml @@ -12,7 +12,7 @@ plugins/MOG/html/images/icon.png 2 - Squeezebox Server + Logitech Media Server 7.5 * diff --git a/Slim/Plugin/MP3tunes/Plugin.pm b/Slim/Plugin/MP3tunes/Plugin.pm index a5ae916a8b6..b90105bef4b 100644 --- a/Slim/Plugin/MP3tunes/Plugin.pm +++ b/Slim/Plugin/MP3tunes/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::MP3tunes::Plugin; -# $Id$ # Browse MP3tunes via SqueezeNetwork @@ -15,7 +14,7 @@ sub initPlugin { my $class = shift; Slim::Player::ProtocolHandlers->registerIconHandler( - qr/(?:squeezenetwork\.com.*\/mp3tunes|mp3tunes\.com\/)/, + qr/(?:mysqueezebox\.com.*\/mp3tunes|mp3tunes\.com\/)/, sub { return $class->_pluginDataFor('icon'); } ); @@ -28,7 +27,7 @@ sub initPlugin { ); Slim::Formats::RemoteMetadata->registerProvider( - match => qr{mp3tunes\.com|squeezenetwork\.com/mp3tunes}, + match => qr{mp3tunes\.com|mysqueezebox\.com/mp3tunes}, func => \&metaProvider, ); } diff --git a/Slim/Plugin/MusicMagic/ClientSettings.pm b/Slim/Plugin/MusicMagic/ClientSettings.pm index 17f288f4b97..336d739577c 100644 --- a/Slim/Plugin/MusicMagic/ClientSettings.pm +++ b/Slim/Plugin/MusicMagic/ClientSettings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::MusicMagic::ClientSettings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/MusicMagic/Common.pm b/Slim/Plugin/MusicMagic/Common.pm index 32fc3d04a6e..8cf0916664e 100644 --- a/Slim/Plugin/MusicMagic/Common.pm +++ b/Slim/Plugin/MusicMagic/Common.pm @@ -1,8 +1,7 @@ package Slim::Plugin::MusicMagic::Common; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech +# Logitech Media Server Copyright 2001-2020 Logitech # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/MusicMagic/Importer.pm b/Slim/Plugin/MusicMagic/Importer.pm index 52d8d4a8e83..8354b3dbf33 100644 --- a/Slim/Plugin/MusicMagic/Importer.pm +++ b/Slim/Plugin/MusicMagic/Importer.pm @@ -1,8 +1,7 @@ package Slim::Plugin::MusicMagic::Importer; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech +# Logitech Media Server Copyright 2001-2020 Logitech # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/MusicMagic/PlayerSettings.pm b/Slim/Plugin/MusicMagic/PlayerSettings.pm index 99b364883dd..3fe3c3e6f84 100644 --- a/Slim/Plugin/MusicMagic/PlayerSettings.pm +++ b/Slim/Plugin/MusicMagic/PlayerSettings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::MusicMagic::PlayerSettings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/MusicMagic/Plugin.pm b/Slim/Plugin/MusicMagic/Plugin.pm index b32469f58ba..4b89602f117 100644 --- a/Slim/Plugin/MusicMagic/Plugin.pm +++ b/Slim/Plugin/MusicMagic/Plugin.pm @@ -1,8 +1,7 @@ package Slim::Plugin::MusicMagic::Plugin; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech +# Logitech Media Server Copyright 2001-2020 Logitech # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -334,7 +333,7 @@ sub postinitPlugin { foreach (@$seedTracks) { my ($trackObj) = Slim::Schema->find('Track', $seedTracks->[0]->{id}); - my $mix = getMix($client, $trackObj->path, 'album') if $trackObj; + my $mix = getMix($client, $trackObj->path, 'track') if $trackObj; main::idleStreams(); @@ -1259,7 +1258,7 @@ sub _prepare_mix { # For the moment, skip straight to InstantMix mode. (See VarietyCombo) $mix = getMix($client, $obj->path, 'track'); - warn Data::Dump::dump($mix); + } $params->{'src_mix'} = Slim::Music::Info::standardTitle(undef, $obj); diff --git a/Slim/Plugin/MusicMagic/ProtocolHandler.pm b/Slim/Plugin/MusicMagic/ProtocolHandler.pm index 465e0f65a0d..c7c19dee53f 100644 --- a/Slim/Plugin/MusicMagic/ProtocolHandler.pm +++ b/Slim/Plugin/MusicMagic/ProtocolHandler.pm @@ -1,8 +1,6 @@ package Slim::Plugin::MusicMagic::ProtocolHandler; -# $Id - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/MusicMagic/Settings.pm b/Slim/Plugin/MusicMagic/Settings.pm index 4f0d2434402..a48416bb627 100644 --- a/Slim/Plugin/MusicMagic/Settings.pm +++ b/Slim/Plugin/MusicMagic/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::MusicMagic::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/MusicMagic/strings.txt b/Slim/Plugin/MusicMagic/strings.txt index 1145cf5d387..d60dcb7d9b8 100644 --- a/Slim/Plugin/MusicMagic/strings.txt +++ b/Slim/Plugin/MusicMagic/strings.txt @@ -1,7 +1,5 @@ # String file for MusicIP plugin -# $Id: strings.txt 11145 2007-01-07 00:19:08Z kdf $ - MUSICMAGIC CS MusicIP DA MusicIP @@ -59,7 +57,7 @@ SETUP_MUSICMAGICPLAYLISTFORMAT_DESC FR Ce réglage détermine le format utilisé lors de l'importation des listes de lecture MusicIP. HE אפשרות זו קובעת את התבנית שזה נעשה שימוש בעת ייבוא רשימות השמעה של MusicIP. IT Questa opzione consente di specificare il formato utilizzato per l'importazione delle playlist di MusicIP. - NL Deze optie bepaalt welk formaat bij het importeren van MusicIP-playlists gebruikt wordt. + NL Deze optie bepaalt welk formaat bij het importeren van MusicIP-playlists wordt gebruikt. NO Denne innstillingen avgjør hvilket format som brukes ved importering av MusicIP-spillelister. PL Ta opcja określa format używany podczas importowania list odtwarzania z usługi MusicIP. RU Этот параметр определяет формат для импорта плей-листов MusicIP. @@ -229,7 +227,7 @@ SETUP_REJECT_SIZE_DESC FR Définissez le nombre d'éléments à ignorer avant de répéter un type. La valeur par défaut est 12. HE הגדר את מספר הפריטים שיש לדלג עליהם לפני חזרה על סוג. (ברירת המחדל: 12) IT Consente di impostare il numero di elementi da saltare prima di ripetere un tipo. Il valore predefinito è 12. - NL Slet het aantal items in dat overgeslagen moet worden, voordat een type herhaald wordt (standaard: 12). + NL Stel het aantal items in dat overgeslagen moet worden, voordat een type wordt herhaald (standaard: 12). NO Angi hvor mange elementer som skal hoppes over før en type gjentas. (Standard: 12) PL Ustaw liczbę elementów do pominięcia przed powtórzeniem typu (domyślnie: 12). RU Укажите количество элементов, которые следует пропустить перед повторением типа. (По умолчанию: 12) @@ -331,7 +329,7 @@ SETUP_MMMPLAYERSETTINGS FR Réglages de la platine pour chaque mix HE הגדרות נגן עבור כל מיקס IT Impostazioni del lettore per ogni raccolta - NL Muzieksysteeminstellingen voor elke mix + NL Muziekspelerinstellingen voor elke mix NO Spillerinnstillinger for hver miks PL Ustawienia odtwarzacza dla każdej składanki RU Настройки плеера для каждого микса @@ -348,7 +346,7 @@ SETUP_MMMPLAYERSETTINGS_DESC FR Vous pouvez choisir de donner à chaque platine accès aux réglages du mix MusicIP avant de générer un mix. Tous les réglages définis à partir de la platine remplacent les réglages du serveur, jusqu'à ce qu'ils soient à nouveau modifiés à partir de la platine. Si vous souhaitez passer cette étape et créer directement un mix à l'aide des paramètres du serveur ou de la platine existants, sélectionnez non. HE באפשרותך לבחור להעניק לכל נגן גישה להגדרות המיקס של MusicIP לפני הפקת מיקס. הגדרות שנבחרו בנגן יעקפו הגדרות שנבחרו בשרת ויישארו בתוקף עד שישונו שוב בנגן. אם ברצונך לדלג ישירות לשלב של יצירת מיקס, באמצעות ההגדרות הנוכחיות של הנגן/השרת בלבד, בחר 'לא'. IT È possibile impostare i singoli lettori per l'accesso alle impostazioni del mix MusicIP prima di creare il mix. Le impostazioni del lettore sostituiscono quelle del server e vengono mantenute fino a quando non vengono eseguite altre modifiche utilizzando il lettore stesso. Se si desidera passare direttamente alla creazione di un mix, utilizzare solo le impostazioni del lettore/server esistenti, quindi scegliere no. - NL Je kunt ervoor kiezen elk muzieksysteem toegang te geven tot de MusicIP-mixinstellingen voordat er een mix gegenereerd wordt. Muzieksysteeminstellingen vervangen de serverinstellingen en blijven van kracht tot ze via het systeem veranderd worden. Kies 'Nee' als je direct een mix wilt maken met de huidige systeem-/serverinstellingen. + NL Je kunt ervoor kiezen elke muziekspeler toegang te geven tot de MusicIP-mixinstellingen voordat er een mix wordt gegenereerd. Instellingen op de muziekspeler gaan voor op de serverinstellingen en blijven van kracht tot ze via het systeem worden gewijzigd. Kies 'Nee' als je direct een mix wilt maken met de huidige systeem-/serverinstellingen. NO Du kan velge å gi hver spiller tilgang til mikseinnstillingene til MusicIP før du lager en miks. Enhver brukervalgt innstilling overstyrer serverinnstillinger, og brukervalgte innstillinger forblir aktive til de endres igjen fra spilleren. Hvis du vil gå rett til å lage en miks med bare eksisterende spiller- og serverinnstillinger, velger du nei. PL Przed utworzeniem składanki każdemu odtwarzaczowi można umożliwić uzyskanie dostępu do ustawień składanki w usłudze MusicIP. Ustawienia wybrane w odtwarzaczu zastąpią ustawienia na serwerze i pozostaną stałe do momentu ponownej zmiany w odtwarzaczu. Jeżeli chcesz od razu przejść do tworzenia składanki z wykorzystaniem istniejących ustawień odtwarzacza/serwera, wybierz opcję Nie. RU Перед созданием микса можно предоставить каждому плееру доступ к настройкам микса MusicIP. Настройки, заданные через плеер, переопределяют настройки сервера и сохраняются, пока вы снова не измените их через плеер. Чтобы сразу перейти к созданию микса, используя настройки плеера/сервера, выберите "Нет". @@ -535,7 +533,7 @@ SETUP_MUSICMAGICSCANINTERVAL_DESC FR Le Logitech Media Server importe automatiquement les informations de la base de données MusicIP lorsque vous la modifiez. Vous pouvez spécifier le délai minimal en secondes au bout duquel le Logitech Media Server actualise la base de données MusicIP. La valeur 0 désactive cette fonctionnalité. HE כאשר מסד הנתונים של MusicIP משתנה, Logitech Media Server מייבא את מידע הספרייה באופן אוטומטי. באפשרותך לציין כמות זמן מינימלית (בשניות) שבמהלכה על Logitech Media Server להמתין לפני טעינה חוזרת של מסד הנתונים של MusicIP. ערך אפס משבית את אפשרות הטעינה מחדש. IT Se si modifica il database di MusicIP, Logitech Media Server importa automaticamente i dati della libreria. È possibile specificare per Logitech Media Server il periodo minimo di attesa (in secondi) precedente al ricaricamento del database di MusicIP. Se si imposta il valore zero, la funzionalità viene disattivata. - NL Wanneer je MusicIP-database verandert, worden de collectiegegevens automatisch door Logitech Media Server geïmporteerd. Je kunt opgeven (in seconden) hoe lang Logitech Media Server minimaal moet wachten alvorens je MusicIP-database te herladen. Een waarde van 0 seconden schakelt herladen uit. + NL Wanneer je MusicIP-database wijzigt, worden de collectiegegevens automatisch door Logitech Media Server geïmporteerd. Je kunt opgeven (in seconden) hoe lang Logitech Media Server minimaal moet wachten alvorens je MusicIP-database te herladen. Een waarde van 0 seconden schakelt herladen uit. NO Når databasen til MusicIP endres, importerer Logitech Media Server automatisk bibliotekinformasjonen. Du kan angi en minimumstid (i sekunder) for hvor lenge Logitech Media Server skal vente med å laste inn databasen til MusicIP igjen. Hvis verdien er null, deaktiveres innlasting. PL Po zmianie informacji w bazie danych usługi MusicIP program Logitech Media Server automatycznie zaimportuje informacje o bibliotece. Możliwe jest określenie minimalnego czasu (w sekundach) po jakim program Logitech Media Server ponownie załaduje bazę danych usługi MusicIP. Ustawienie wartości zero powoduje wyłączenie ponownego ładowania. RU При изменении базы данных MusicIP данные медиатеки автоматически импортируются в Logitech Media Server. Для Logitech Media Server можно задать минимальный интервал времени (в секундах) перед повторной загрузкой базы данных MusicIP. Значение 0 означает отмену повторной загрузки. @@ -569,7 +567,7 @@ SETUP_MMSPORT_DESC FR L'API du service MusicIP permet de sélectionner un port HTTP à utiliser pour les requêtes. Saisissez ici le numéro de port spécifié dans les réglages MusicIP Mixer. HE ממשק תוכנת היישום (API) של שירות MusicIP מאפשרת לבחור יציאת http לשימוש עבור שאילתות ל-API של MusicIP. הזן בשדה זה את מספר היציאה בהתאם להגדרה שבחרת בהגדרות יוצר המיקסים של MusicIP. IT La API del servizio MusicIP consente di selezionare una porta HTTP da utilizzare per l'invio di query all'API di MusicIP. Inserire qui il numero di porta corrispondente a quello indicato nelle impostazioni della raccolta MusicIP. - NL De MusicIP-service API biedt de mogelijkheid om een HTTP-poort te gebruiken voor vragen aan de MusicIP-API Geef hier het poortnummer op dat je in de MusicIP-mixerinstellingen hebt gekozen. + NL De MusicIP-service API biedt de mogelijkheid om een HTTP-poort te gebruiken voor vragen aan de MusicIP-API. Geef hier het poortnummer op dat je in de MusicIP-mixerinstellingen hebt gekozen. NO I tjeneste-API-en for MusicIP kan du velge en http-port til henvendelser til MusicIP-API-en. Angi det samme portnummeret her som du har angitt i innstillingene for MusicIP-mikseren. PL Interfejs API usługi MusicIP umożliwia wybranie portu http używanego w celu wysyłania zapytań do tego interfejsu. Wprowadź tutaj numer portu zgodny z wybranymi miksera w usłudze MusicIP. RU Интерфейс MusicIP Service API позволяет выбрать НТТР-порт для запросов к MusicIP API. Введите здесь номер порта, соответствующий вашим настройкам MusicIP Mixer. @@ -619,7 +617,7 @@ DEBUG_PLUGIN_MUSICIP FI MusicIP-miksin ja -viennin kirjaus FR Journalisation des mix et exportations MusicIP IT Registrazione esportazione e mix MusicIP - NL Registratie van MusicIP-mix en -export + NL Loggen van MusicIP-mix en -export NO Loggføring av miksing og eksportering for MusicIP PL Rejestrowane składanki w usłudze MusicIP Mix i eksportu RU Ведение журнала экспорта и микса MusicIP diff --git a/Slim/Plugin/MyApps/Plugin.pm b/Slim/Plugin/MyApps/Plugin.pm index 4fc97881d2a..4741cf9bbc6 100644 --- a/Slim/Plugin/MyApps/Plugin.pm +++ b/Slim/Plugin/MyApps/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::MyApps::Plugin; -# $Id$ use strict; use base qw(Slim::Plugin::OPMLBased); diff --git a/Slim/Plugin/NetTest/Plugin.pm b/Slim/Plugin/NetTest/Plugin.pm index c0c6dad3b9b..441584e1724 100644 --- a/Slim/Plugin/NetTest/Plugin.pm +++ b/Slim/Plugin/NetTest/Plugin.pm @@ -1,10 +1,9 @@ # Plugin for Logitech Media Server to test network bandwidth # -# $Id$ # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2005-2011 Logitech. +# Logitech Media Server Copyright 2005-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/NetTest/strings.txt b/Slim/Plugin/NetTest/strings.txt index f0bfb2e0a40..13bd6b1084e 100644 --- a/Slim/Plugin/NetTest/strings.txt +++ b/Slim/Plugin/NetTest/strings.txt @@ -1,6 +1,5 @@ # String file for NetTest plugin -# $Id$ PLUGIN_NETTEST CS Test sítě @@ -75,7 +74,7 @@ PLUGIN_NETTEST_NOT_SUPPORTED FR Non pris en charge par cette platine HE לא נתמך בנגן זה IT Non supportato in questo lettore - NL Niet ondersteund op dit muzieksysteem + NL Niet ondersteund op deze muziekspeler NO Ikke støttet på denne spilleren PL Nieobsługiwane w tym odtwarzaczu RU Не поддерживается в этом плеере diff --git a/Slim/Plugin/OPMLBased.pm b/Slim/Plugin/OPMLBased.pm index d792e724e20..64fc00fe4d7 100644 --- a/Slim/Plugin/OPMLBased.pm +++ b/Slim/Plugin/OPMLBased.pm @@ -1,6 +1,5 @@ package Slim::Plugin::OPMLBased; -# $Id$ # Base class for all plugins that use OPML feeds diff --git a/Slim/Plugin/OPMLGeneric/Plugin.pm b/Slim/Plugin/OPMLGeneric/Plugin.pm index 5e05c8ec0d1..a53c293288c 100644 --- a/Slim/Plugin/OPMLGeneric/Plugin.pm +++ b/Slim/Plugin/OPMLGeneric/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::OPMLGeneric::Plugin; -# $Id$ use strict; use base qw(Slim::Plugin::OPMLBased); diff --git a/Slim/Plugin/Orange/Metadata.pm b/Slim/Plugin/Orange/Metadata.pm index 1b36e4d918d..f65ed5d8c69 100644 --- a/Slim/Plugin/Orange/Metadata.pm +++ b/Slim/Plugin/Orange/Metadata.pm @@ -1,6 +1,9 @@ package Slim::Plugin::Orange::Metadata; -# $Id: Metadata.pm 10553 2011-05-06 15:29:58Z mherger $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/Orange/Plugin.pm b/Slim/Plugin/Orange/Plugin.pm index ad652727ee6..6e8921d0e63 100644 --- a/Slim/Plugin/Orange/Plugin.pm +++ b/Slim/Plugin/Orange/Plugin.pm @@ -1,6 +1,9 @@ package Slim::Plugin::Orange::Plugin; -# $Id: Plugin.pm 10712 2011-06-29 10:26:15Z shaahul21 $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; use base qw(Slim::Plugin::OPMLBased); @@ -23,7 +26,7 @@ sub initPlugin { Slim::Plugin::Orange::Metadata->init(); Slim::Player::ProtocolHandlers->registerIconHandler( - qr|squeezenetwork\.com.*/api/orange/|, + qr|mysqueezebox\.com.*/api/orange/|, sub { $class->_pluginDataFor('icon') } ); diff --git a/Slim/Plugin/Orange/install.xml b/Slim/Plugin/Orange/install.xml index 2b191b9537f..a7a09231d05 100644 --- a/Slim/Plugin/Orange/install.xml +++ b/Slim/Plugin/Orange/install.xml @@ -12,7 +12,7 @@ plugins/Orange/html/images/icon.png 2 - Squeezebox Server + Logitech Media Server 7.5 * diff --git a/Slim/Plugin/Pandora/Plugin.pm b/Slim/Plugin/Pandora/Plugin.pm index f92822c9787..1e865bc73f0 100644 --- a/Slim/Plugin/Pandora/Plugin.pm +++ b/Slim/Plugin/Pandora/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Pandora::Plugin; -# $Id$ # Play Pandora via mysqueezebox.com @@ -223,6 +222,7 @@ sub trackInfoMenu { } } +=pod sub artistInfoMenu { my ( $client, $url, $artist, $remoteMeta ) = @_; @@ -246,6 +246,7 @@ sub artistInfoMenu { }; } } +=cut sub stationDeleted { my $request = shift; diff --git a/Slim/Plugin/Pandora/ProtocolHandler.pm b/Slim/Plugin/Pandora/ProtocolHandler.pm index 1e47948946a..3a1c90b1b8a 100644 --- a/Slim/Plugin/Pandora/ProtocolHandler.pm +++ b/Slim/Plugin/Pandora/ProtocolHandler.pm @@ -1,6 +1,9 @@ package Slim::Plugin::Pandora::ProtocolHandler; -# $Id: ProtocolHandler.pm 11678 2007-03-27 14:39:22Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. # Handler for pandora:// URLs @@ -40,7 +43,7 @@ sub new { url => $streamUrl, song => $args->{'song'}, client => $client, - bitrate => 128_000, + bitrate => $song->bitrate() || 128_000, } ) || return; ${*$sock}{contentType} = 'audio/mpeg'; @@ -202,6 +205,7 @@ sub gotNextTrack { } # Save metadata for this track + $song->bitrate( $track->{bitrate} ); $song->duration( $track->{secs} ); $song->pluginData( $track ); $song->streamUrl($track->{'audioUrl'}); @@ -239,7 +243,7 @@ sub getSeekData { my ( $class, $client, $song, $newtime ) = @_; return { - sourceStreamOffset => ( 128_000 / 8 ) * $newtime, + sourceStreamOffset => ( ($song->bitrate || 128_000) / 8 ) * $newtime, timeOffset => $newtime, }; } @@ -250,7 +254,7 @@ sub parseDirectHeaders { my $url = shift; my @headers = @_; - my $bitrate = 128_000; + my $bitrate = $client->streamingSong->bitrate || 128_000; my $contentType = 'mp3'; # Clear previous duration, since we're using the same URL for all tracks @@ -389,32 +393,6 @@ sub trackGain { return $gain; } -# Track Info menu -=pod XXX - legacy track info menu from before Slim::Menu::TrackInfo times? -sub trackInfo { - my ( $class, $client, $track ) = @_; - - my $url = $track->url; - - # SN URL to fetch track info menu - my $trackInfoURL = $class->trackInfoURL( $client, $url ); - - # let XMLBrowser handle all our display - my %params = ( - header => 'PLUGIN_PANDORA_GETTING_TRACK_DETAILS', - modeName => 'Pandora Now Playing', - title => Slim::Music::Info::getCurrentTitle( $client, $url ), - url => $trackInfoURL, - remember => 0, - timeout => 35, - ); - - Slim::Buttons::Common::pushMode( $client, 'xmlbrowser', \%params ); - - $client->modeParam( 'handledTransition', 1 ); -} -=cut - # URL used for CLI trackinfo queries sub trackInfoURL { my ( $class, $client, $url ) = @_; @@ -447,13 +425,15 @@ sub getMetadataFor { my $icon = $class->getIcon(); + my $bitrate = $song->bitrate ? ($song->bitrate/1000) . 'k CBR' : '128k CBR'; + # Could be somewhere else in the playlist if ($song->track->url ne $url) { main::DEBUGLOG && $log->debug($url); return { icon => $icon, cover => $icon, - bitrate => '128k CBR', + bitrate => $bitrate, type => 'MP3 (Pandora)', title => 'Pandora', album => Slim::Music::Info::standardTitle( $client, $url, undef ), @@ -470,7 +450,7 @@ sub getMetadataFor { icon => $icon, replay_gain => $track->{trackGain}, duration => $track->{secs}, - bitrate => '128k CBR', + bitrate => $bitrate, type => 'MP3 (Pandora)', info_link => 'plugins/pandora/trackinfo.html', buttons => { @@ -500,7 +480,7 @@ sub getMetadataFor { return { icon => $icon, cover => $icon, - bitrate => '128k CBR', + bitrate => $bitrate, type => 'MP3 (Pandora)', title => $song->track()->title(), }; diff --git a/Slim/Plugin/Podcast/Parser.pm b/Slim/Plugin/Podcast/Parser.pm index 970900b6def..bc7486dcac9 100644 --- a/Slim/Plugin/Podcast/Parser.pm +++ b/Slim/Plugin/Podcast/Parser.pm @@ -38,7 +38,7 @@ sub parse { $item->{'xmlns:slim'} = 1; # some podcasts come with formatted duration ("00:54:23") - convert into seconds - my $duration = $item->{duration}; + my $duration = $item->{duration} || ''; $duration =~ s/00:(\d\d:\d\d)/$1/; my ($s, $m, $h) = strptime($item->{duration} || 0); @@ -65,6 +65,10 @@ sub parse { # fall back to cached value - if available $item->{duration} ||= $cache->get("$key-duration"); + + if ( $item->{duration} && $item->{duration} =~ /(\d+):(\d)/ ) { + $item->{duration} = $1*60 + $2; + } } $cache->set("$key-duration", $item->{duration}, '30days'); @@ -82,7 +86,7 @@ sub parse { _scanItem(); } - my $progress = $client->symbols($client->progressBar(12, $position ? 1 : 0, 0)); + my $progress = $client->symbols($client->progressBar(12, $position ? 1 : 0, 0)) if $client && !$client->display->isa('Slim::Display::NoDisplay'); # if we've played this podcast before, add a menu level to ask whether to continue or start from scratch if ( $position && $position < $item->{duration} - 15 ) { @@ -117,7 +121,7 @@ sub parse { $item->{type} = 'link'; - $progress = $client->symbols($client->progressBar(12, 0.5, 0)); + $progress = $client->symbols($client->progressBar(12, 0.5, 0)) if $client && !$client->display->isa('Slim::Display::NoDisplay'); } $item->{title} = $progress . ' ' . $item->{title} if $progress; diff --git a/Slim/Plugin/Podcast/Plugin.pm b/Slim/Plugin/Podcast/Plugin.pm index 6ac7e0b3fee..5d59446a6fd 100644 --- a/Slim/Plugin/Podcast/Plugin.pm +++ b/Slim/Plugin/Podcast/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::Podcast::Plugin; -# Copyright 2005-2013 Logitech +# Logitech Media Server Copyright 2005-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, @@ -109,8 +109,8 @@ sub songChangeCallback { } my $url = Slim::Player::Playlist::url($client); - - if ( $request->isCommand([['playlist'], ['newsong']]) && $client->pluginData('goto') ne $url && Slim::Music::Info::isRemoteURL($url) ) { + + if ( $request->isCommand([['playlist'], ['newsong']]) && !($client->pluginData('goto') && $client->pluginData('goto') eq $url) && Slim::Music::Info::isRemoteURL($url) ) { $client->pluginData( goto => $url ); if ( my $newPos = $cache->get("podcast-$url") ) { diff --git a/Slim/Plugin/Podcast/Settings.pm b/Slim/Plugin/Podcast/Settings.pm index 586fd86fc6f..70a04c381e7 100644 --- a/Slim/Plugin/Podcast/Settings.pm +++ b/Slim/Plugin/Podcast/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::Podcast::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -11,7 +11,7 @@ use base qw(Slim::Web::Settings); use Slim::Utils::Favorites; use Slim::Utils::Log; use Slim::Utils::Prefs; -use Slim::Utils::Strings; +use Slim::Utils::Strings qw(string); my $log = logger('plugin.podcast'); my $prefs = preferences('plugin.podcast'); diff --git a/Slim/Plugin/Podcast/strings.txt b/Slim/Plugin/Podcast/strings.txt index 593bce7ef0f..5e6e1b2ee07 100644 --- a/Slim/Plugin/Podcast/strings.txt +++ b/Slim/Plugin/Podcast/strings.txt @@ -1,7 +1,5 @@ # String file for Podcast plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_PODCAST CS Podcasty DA Podcasts diff --git a/Slim/Plugin/PresetsEditor/HTML/EN/settings/presets.html b/Slim/Plugin/PresetsEditor/HTML/EN/settings/presets.html new file mode 100644 index 00000000000..5377b09ba33 --- /dev/null +++ b/Slim/Plugin/PresetsEditor/HTML/EN/settings/presets.html @@ -0,0 +1,53 @@ +[% PROCESS settings/header.html %] + + [% WRAPPER setting title="PLUGIN_PRESETS_EDITOR" desc="PLUGIN_PRESETS_EDITOR_DESC" %] +
+ [% FOREACH preset = presets %] +
+ [% loop.count %]. + + + + +
+ [% END %] +
+ [% END %] + + +[% PROCESS settings/footer.html %] \ No newline at end of file diff --git a/Slim/Plugin/PresetsEditor/Plugin.pm b/Slim/Plugin/PresetsEditor/Plugin.pm new file mode 100644 index 00000000000..47c3d468dea --- /dev/null +++ b/Slim/Plugin/PresetsEditor/Plugin.pm @@ -0,0 +1,28 @@ +package Slim::Plugin::PresetsEditor::Plugin; + +# Logitech Media Server Copyright 2001-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + +use strict; + +use Slim::Utils::Log; + +my $log = Slim::Utils::Log->addLogCategory({ + 'category' => 'plugin.presetseditor', + 'defaultLevel' => 'ERROR', + 'description' => 'PLUGIN_PRESETS_EDITOR', +}); + +sub initPlugin { + if (main::WEBUI) { + require Slim::Plugin::PresetsEditor::Settings; + Slim::Plugin::PresetsEditor::Settings->new(); + } + else { + $log->warn(Slim::Utils::Strings::string('PLUGIN_PRESETS_EDITOR_NEED_WEBUI')); + } +} + +1; \ No newline at end of file diff --git a/Slim/Plugin/PresetsEditor/Settings.pm b/Slim/Plugin/PresetsEditor/Settings.pm new file mode 100644 index 00000000000..d70568f829c --- /dev/null +++ b/Slim/Plugin/PresetsEditor/Settings.pm @@ -0,0 +1,75 @@ +package Slim::Plugin::PresetsEditor::Settings; + +# Logitech Media Server Copyright 2001-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + +use strict; + +use base qw(Slim::Web::Settings); +use JSON::XS::VersionOneAndTwo; + +use Slim::Utils::Alarm; +use Slim::Utils::Log; +use Slim::Utils::Prefs; + +my $prefs = preferences('server'); +my $log = logger('plugin.presetseditor'); + +sub name { + return Slim::Web::HTTP::CSRF->protectName('PLUGIN_PRESETS_EDITOR'); +} + +sub needsClient { 1 } + +sub page { + return Slim::Web::HTTP::CSRF->protectURI('settings/presets.html'); +} + +sub handler { + my ($class, $client, $params) = @_; + + if ($params->{'saveSettings'}) { + my $presets = $prefs->client($client)->get('presets'); + + foreach (1..10) { + my $text = $params->{'preset_text_' . $_}; + my $url = $params->{'preset_url_' . $_}; + + if ($url ne $presets->[$_-1]->{URL} || ($text && $text ne $presets->[$_-1]->{text})) { + $text = '' if !$url; + $presets->[$_-1] = { + URL => $url, + text => $text || '', + type => 'audio' + }; + + $prefs->client($client)->set('presets', $presets); + } + } + } + + $params->{presets} = $client ? $prefs->client($client)->get('presets') : []; + + my $playlistOptions = Slim::Utils::Alarm->getPlaylists($client); + my %urlToName; + + foreach my $category (@$playlistOptions) { + my $items = [ grep { + $urlToName{$_->{url}} = $_->{title}; + $_->{url} ; + } @{$category->{items}} ]; + $category->{items} = $items; + } + + $params->{playlistOptions} = [ grep { + $_->{items} && scalar @{$_->{items}} + } @$playlistOptions ]; + + $params->{urlToName} = to_json(\%urlToName); + + return $class->SUPER::handler( $client, $params ); +} + +1; \ No newline at end of file diff --git a/Slim/Plugin/PresetsEditor/install.xml b/Slim/Plugin/PresetsEditor/install.xml new file mode 100644 index 00000000000..e9da52d595b --- /dev/null +++ b/Slim/Plugin/PresetsEditor/install.xml @@ -0,0 +1,20 @@ + + + + c7171249-fcde-43bc-97e5-1be92854d137 + PLUGIN_PRESETS_EDITOR + Slim::Plugin::PresetsEditor::Plugin + 1.0 + PLUGIN_PRESETS_EDITOR_DESC + Logitech + + enabled + 2 + + + Logitech Media Server + 7.7.0 + * + + + diff --git a/Slim/Plugin/PresetsEditor/strings.txt b/Slim/Plugin/PresetsEditor/strings.txt new file mode 100644 index 00000000000..0d1bcc5f8be --- /dev/null +++ b/Slim/Plugin/PresetsEditor/strings.txt @@ -0,0 +1,11 @@ +PLUGIN_PRESETS_EDITOR + DE Presets bearbeiten + EN Presets Editor + +PLUGIN_PRESETS_EDITOR_DESC + DE Dieses Plugin erlaubt es ihnen, die Preset-Tasten deiner Squeezebox zu konfigurieren. + EN This plugin allows you to visually edit the configuration of your Squeezebox preset keys. + +PLUGIN_PRESETS_EDITOR_NEED_WEBUI + DE Um den Presets Editor verwenden zu können müssen Sie das Web-Interface aktivieren. + EN You need to enable the web UI in order to use the Presets Editor. diff --git a/Slim/Plugin/PreventStandby/OSX.pm b/Slim/Plugin/PreventStandby/OSX.pm index fda42652286..aa3fc95dbdf 100644 --- a/Slim/Plugin/PreventStandby/OSX.pm +++ b/Slim/Plugin/PreventStandby/OSX.pm @@ -1,6 +1,6 @@ package Slim::Plugin::PreventStandby::OSX; -# Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/PreventStandby/Plugin.pm b/Slim/Plugin/PreventStandby/Plugin.pm index b19a70c8468..a00a43f27ef 100644 --- a/Slim/Plugin/PreventStandby/Plugin.pm +++ b/Slim/Plugin/PreventStandby/Plugin.pm @@ -1,34 +1,14 @@ package Slim::Plugin::PreventStandby::Plugin; -# $Id: Plugin.pm 11021 2006-12-21 22:28:39Z dsully $ +# Logitech Media Server Copyright 2006-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. # PreventStandby.pm by Julian Neil (julian.neil@internode.on.net) -# + # Prevent the server machine from going into standby when it is streaming # music to any clients. -# -# Excuse my perl.. first time I've ever used it. -# -# Thanks to the PowerSave plugin by Jason Holtzapplefor some basics, -# to various ppl on the slim forums and to CPAN and the Win32::API module. -# -#-> Changelog -# -# 1.0 - 2006-04-05 - Initial Release -# -# 2.0 - 2009-01-03 - Proposed changes by Gordon Harris to address bug 8520: -# -# http://bugs.slimdevices.com/show_bug.cgi?id=8520 -# -# Added "idletime" feature -- waits at least $idletime number -# of idle player intervals before allowing standby. Also, is -# "resume aware" -- resets the idle counter on system resume -# from standby or hibernation. -# -# 2009-01-12 - Cleaned up some content in strings.txt, added optional check -# power feature to mimic Nigel Burch's proposed patch behavior. -# -# 3.0 - 2012-08-26 - add support for OSX, plus infrastructure to add more use strict; use Time::HiRes; diff --git a/Slim/Plugin/PreventStandby/Settings.pm b/Slim/Plugin/PreventStandby/Settings.pm index 7ec9847452e..cf2001347ba 100644 --- a/Slim/Plugin/PreventStandby/Settings.pm +++ b/Slim/Plugin/PreventStandby/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::PreventStandby::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/PreventStandby/Win32.pm b/Slim/Plugin/PreventStandby/Win32.pm index 428eef6b324..b0d3ee5dd79 100644 --- a/Slim/Plugin/PreventStandby/Win32.pm +++ b/Slim/Plugin/PreventStandby/Win32.pm @@ -1,6 +1,6 @@ package Slim::Plugin::PreventStandby::Win32; -# Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/PreventStandby/strings.txt b/Slim/Plugin/PreventStandby/strings.txt index 6999a3dee46..f92af2a75f9 100644 --- a/Slim/Plugin/PreventStandby/strings.txt +++ b/Slim/Plugin/PreventStandby/strings.txt @@ -84,7 +84,7 @@ PLUGIN_PREVENTSTANDBY_PLAYERSON_DESC FI Ottamalla tämän asetuksen käyttöön estät järjestelmää siirtymästä valmiustilaan soitinten ollessa päällä, eli silloin, kun kelloa ei näy. FR Activez ce réglage pour interdire la mise en veille du système lorsque des platines sont allumées (l'horloge n'est pas affichée). IT Attivare questa impostazione per impedire l'attivazione della modalità standby quando i lettori sono accesi, vale a dire quando non visualizzano l'orologio. - NL Schakel deze instelling in om te voorkomen dat het systeem naar stand-by gaat terwijl er muzieksystemen actief zijn, d.w.z. niet de klok weergeven. + NL Schakel deze instelling in om te voorkomen dat het systeem naar stand-by gaat terwijl er muziekspelers actief zijn, d.w.z. niet de klok weergeven. NO Når denne innstillingen er aktivert, forhindrer den systemet fra å gå hvilemodus mens spillerne er slått på (altså når de ikke viser klokken). PL Włącz to ustawienie, aby uniemożliwić tryb wstrzymania systemu, kiedy odtwarzacze są włączone (tzn. nie wyświetlają zegara). RU Включите этот параметр, чтобы запретить переход в режим ожидания во время работы плееров (не отображать часы). diff --git a/Slim/Plugin/RS232/Plugin.pm b/Slim/Plugin/RS232/Plugin.pm index ee105199f39..758c7306ab4 100644 --- a/Slim/Plugin/RS232/Plugin.pm +++ b/Slim/Plugin/RS232/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RS232::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/RS232/Settings.pm b/Slim/Plugin/RS232/Settings.pm index 77b65229201..065cd0b2f46 100755 --- a/Slim/Plugin/RS232/Settings.pm +++ b/Slim/Plugin/RS232/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RS232::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/RS232/strings.txt b/Slim/Plugin/RS232/strings.txt index 79b195f3fe9..015705d251a 100644 --- a/Slim/Plugin/RS232/strings.txt +++ b/Slim/Plugin/RS232/strings.txt @@ -1,7 +1,5 @@ # String file for RS232 plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_RS232_NAME CS RS232 DA RS232 diff --git a/Slim/Plugin/RSSNews/Plugin.pm b/Slim/Plugin/RSSNews/Plugin.pm index 2e72a74188c..401e1083142 100644 --- a/Slim/Plugin/RSSNews/Plugin.pm +++ b/Slim/Plugin/RSSNews/Plugin.pm @@ -1,7 +1,7 @@ package Slim::Plugin::RSSNews::Plugin; # RSS News Browser -# Copyright 2006-2009 Logitech +# Logitech Media Server Copyright 2006-2020 Logitech. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, @@ -9,8 +9,6 @@ package Slim::Plugin::RSSNews::Plugin; # # This is a reimplementation of the old RssNews plugin based on # the Podcast Browser plugin. -# -# $Id: Plugin.pm 11071 2007-01-01 15:47:59Z adrian $ use strict; use base qw(Slim::Plugin::Base); diff --git a/Slim/Plugin/RSSNews/Settings.pm b/Slim/Plugin/RSSNews/Settings.pm index 236aae6fa94..87ccb559acd 100644 --- a/Slim/Plugin/RSSNews/Settings.pm +++ b/Slim/Plugin/RSSNews/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RSSNews::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/RSSNews/strings.txt b/Slim/Plugin/RSSNews/strings.txt index 167ea6b3c71..0e89a4c705b 100644 --- a/Slim/Plugin/RSSNews/strings.txt +++ b/Slim/Plugin/RSSNews/strings.txt @@ -1,7 +1,5 @@ # String file for RSSNews plugin -# $Id: strings.txt 11049 2006-12-28 09:47:38Z mherger $ - PLUGIN_RSSNEWS CS Proužek informačního kanálu RSS DA RSS-lysavis med nyheder @@ -283,7 +281,7 @@ SETUP_GROUP_PLUGIN_RSSNEWS_DESC FR Le plugin Agrégateur RSS vous permet de parcourir et d'afficher le contenu de flux RSS. Les paramètres ci-dessous permettent de sélectionner les flux RSS et de modifier leur affichage sur la platine. Cliquez sur Modifier une fois les modifications effectuées. HE ניתן להשתמש ביישום ה-Plugin 'טיקר RSS של חדשות' לעיון בפריטים מהזנות RSS ולהצגתם. ניתן להשתמש בהעדפות שלהלן כדי לקבוע באילו הזנות RSS להשתמש וכדי לשלוט באופן ההצגה שלהן. לסיום, לחץ על הלחצן 'שנה'. IT È possibile utilizzare il plugin RSS News Ticker per sfogliare e visualizzare argomenti dei feed RSS. Per specificare i feed RSS da utilizzare e impostarne il tipo di visualizzazione, utilizzare le preferenze indicate di seguito. Al termine delle operazioni, fare clic sul pulsante Cambia. - NL Met de plug-in voor RSS-nieuwsberichten kun je items van RSS-kanalen bekijken en weergeven. Met de instellingen hieronder kun je bepalen welke RSS-kanalen je wilt gebruiken en hoe ze worden weergegeven. Klik op de knop 'Veranderen' wanneer je klaar bent. + NL Met de plug-in voor RSS-nieuwsberichten kun je items van RSS-kanalen bekijken en weergeven. Met de instellingen hieronder kun je bepalen welke RSS-kanalen je wilt gebruiken en hoe ze worden weergegeven. Klik op de knop 'Wijzigen' wanneer je klaar bent. NO Plugin-modulen rss-nyhetstelegraf kan brukes til å lese og vise elementer fra rss-feeder. Du kan bruke innstillingene nedenfor til å avgjøre hvilke rss-feeder du vil bruke, og kontrollere hvordan de vises. Klikk på Endre når du er ferdig. PL Dodatek Wiadomości RSS umożliwia przeglądanie i wyświetlanie elementów ze źródeł RSS. Poniższe preferencje pozwalają określić, które źródła RSS mają zostać użyte oraz dostosować sposób ich wyświetlania. Po zakończeniu kliknij przycisk Zmień. RU Плагин "Новостной RSS-канал" позволяет просматривать и отображать элементы RSS-каналов. С помощью следующих настроек можно определить перечень RSS-каналов и управлять их отображением. После завершения настройки нажмите кнопку "Изменить". diff --git a/Slim/Plugin/RandomPlay/DontStopTheMusic.pm b/Slim/Plugin/RandomPlay/DontStopTheMusic.pm new file mode 100644 index 00000000000..2855efe5a45 --- /dev/null +++ b/Slim/Plugin/RandomPlay/DontStopTheMusic.pm @@ -0,0 +1,96 @@ +package Slim::Plugin::RandomPlay::DontStopTheMusic; + +# Originally written by Kevin Deane-Freeman (slim-mail (A_t) deane-freeman.com). +# New world order by Dan Sully - +# Fairly substantial rewrite by Max Spicer + +# This code is derived from code with the following copyright message: +# +# Logitech Media Server Copyright 2005-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + +use strict; +use Scalar::Util qw(blessed); +use URI::Escape qw(uri_escape_utf8); + +use Slim::Plugin::DontStopTheMusic::Plugin; +use Slim::Plugin::RandomPlay::Plugin; +use Slim::Utils::Cache; +use Slim::Utils::Log; +use Slim::Utils::Prefs; + +my $cache = Slim::Utils::Cache->new(); +my $log = logger('plugin.randomplay'); +my $prefs = preferences('plugin.randomplay'); + +sub init { + Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_TITLEMIX_WITH_GENRES', sub { + mixWithGenres('track', @_); + }); + + Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_TRACK', sub { + my ($client, $cb) = @_; + $client->execute(['randomplaygenreselectall', 0]); + $cb->($client, ['randomplay://track']); + }); + + Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_ALBUM_MIX_WITH_GENRES', sub { + mixWithGenres('album', @_); + }); + + Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_ALBUM_ITEM', sub { + my ($client, $cb) = @_; + $client->execute(['randomplaygenreselectall', 0]); + $cb->($client, ['randomplay://album']); + }); + + Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_CONTRIBUTOR_ITEM', sub { + my ($client, $cb) = @_; + $client->execute(['randomplaygenreselectall', 0]); + $cb->($client, ['randomplay://contributor']); + }); + + Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_YEAR_ITEM', sub { + my ($client, $cb) = @_; + $client->execute(['randomplaygenreselectall', 0]); + $cb->($client, ['randomplay://year']); + }); +} + +sub mixWithGenres { + my ($type, $client, $cb) = @_; + + return unless $client; + + my %genres; + foreach my $track (@{ Slim::Player::Playlist::playList($client) }) { + if (!blessed $track) { + $track = Slim::Schema->objectForUrl($track); + } + + next unless blessed $track; + + if ( $track->remote ) { + $genres{$track->genre}++ if $track->genre; + } + else { + foreach ( $track->genres ) { + $genres{$_->name}++ + } + } + } + + my $genres = ''; + if (keys %genres) { + $genres = '?genres=' . join(',', map { + uri_escape_utf8($_); + } keys %genres); + } + + $cb->($client, ['randomplay://' . $type . $genres]); +} + + +1; \ No newline at end of file diff --git a/Slim/Plugin/RandomPlay/Mixer.pm b/Slim/Plugin/RandomPlay/Mixer.pm new file mode 100644 index 00000000000..a8ad59b0127 --- /dev/null +++ b/Slim/Plugin/RandomPlay/Mixer.pm @@ -0,0 +1,327 @@ +package Slim::Plugin::RandomPlay::Mixer; + +# Originally written by Kevin Deane-Freeman (slim-mail (A_t) deane-freeman.com). +# New world order by Dan Sully - +# Fairly substantial rewrite by Max Spicer + +# This code is derived from code with the following copyright message: +# +# Logitech Media Server Copyright 2005-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + +use strict; + +use Slim::Utils::Cache; +use Slim::Utils::Log; +use Slim::Utils::Prefs; +use Slim::Utils::Strings qw(string); + +my $cache = Slim::Utils::Cache->new(); +my $log = logger('plugin.randomplay'); +my $prefs = preferences('plugin.randomplay'); + +# Find tracks matching parameters and add them to the playlist +sub findAndAdd { + my ($client, $type, $limit, $addOnly) = @_; + + my $idList = $cache->get('rnd_idList_' . $client->id) || []; + + if ( main::INFOLOG && $log->is_info ) { + $log->info(sprintf("Starting random selection of %s items for type: $type", defined($limit) ? $limit : 'unlimited')); + } + + if ( !scalar @$idList ) { + $idList = getIdList($client, $type); + } + + if ($type eq 'year') { + $type = 'track'; + } + + # get first ID from our randomized list + my @randomIds = splice @$idList, 0, $limit; + + $cache->set('rnd_idList_' . $client->id, $idList, 'never'); + + if (!scalar @randomIds) { + + logWarning("Didn't get a valid object for findAndAdd()!"); + + return undef; + } + + # Add the items to the end + foreach my $id (@randomIds) { + + if ( main::INFOLOG && $log->is_info ) { + $log->info(sprintf("%s %s: #%d", + $addOnly ? 'Adding' : 'Playing', $type, $id + )); + } + + # Replace the current playlist with the first item / track or add it to end + my $request = $client->execute([ + 'playlist', $addOnly ? 'addtracks' : 'loadtracks', sprintf('%s.id=%d', $type, $id) + ]); + + # indicate request source + $request->source('PLUGIN_RANDOMPLAY'); + + $addOnly++; + } +} + +sub getIdList { + my ($client, $type) = @_; + + main::DEBUGLOG && $log->debug('Initialize ID list to be randomized: ' . $type); + + # Search the database for all items of $type which match find criteria + my @joins = (); + + # Initialize find to only include user's selected genres. If they've deselected + # all genres, this clause will be ignored by find, so all genres will be used. + my $filteredGenres = Slim::Plugin::RandomPlay::Plugin::getFilteredGenres($client, 0, 0, 1); + my $excludedGenres = Slim::Plugin::RandomPlay::Plugin::getFilteredGenres($client, 1, 0, 1); + my $queryGenres; + my $queryLibrary; + my $idList; + + # Only look for genre tracks if we have some, but not all + # genres selected. Or no genres selected. + if ( (scalar @$filteredGenres > 0 && scalar @$excludedGenres != 0) || + scalar @$filteredGenres != 0 && scalar @$excludedGenres > 0 ) { + + $queryGenres = join(',', @$filteredGenres); + } + + $queryLibrary = $prefs->get('library') || Slim::Music::VirtualLibraries->getLibraryIdForClient($client); + + if ($type =~ /track|year/) { + # it's messy reaching that far in to Slim::Control::Queries, but it's >5x faster on a Raspberry Pi2 with 100k tracks than running the full "titles" query + (undef, $idList) = Slim::Control::Queries::_getTagDataForTracks( 'II', { + where => '(tracks.content_type != "cpl" AND tracks.content_type != "src" AND tracks.content_type != "ssp" AND tracks.content_type != "dir")', + year => $type eq 'year' && getRandomYear($client, $filteredGenres), + genreId => $queryGenres, + libraryId => $queryLibrary, + } ); + + $type = 'track'; + } + else { + my %categories = ( + album => ['albums', 0, 999_999, 'tags:t'], + contributor => ['artists', 0, 999_999], + track => [] + ); + $categories{year} = $categories{track}; + $categories{artist} = $categories{contributor}; + + my $query = $categories{$type}; + + push @$query, 'genre_id:' . $queryGenres if $queryGenres; + push @$query, 'library_id:' . $queryLibrary if $queryLibrary; + + my $request = Slim::Control::Request::executeRequest($client, $query); + + my $loop = "${type}s_loop"; + $loop = 'artists_loop' if $type eq 'contributor'; + + $idList = [ map { $_->{id} } @{ $request->getResult($loop) || [] } ]; + } + + # shuffle ID list + Slim::Player::Playlist::fischer_yates_shuffle($idList); + + return $idList; +} + +sub getRandomYear { + my $client = shift; + my $filteredGenres = shift; + + main::DEBUGLOG && $log->debug("Starting random year selection"); + + my $years = $cache->get('rnd_years_' . $client->id) || []; + + if (!scalar @$years) { + my %cond = (); + my %attr = ( + 'order_by' => Slim::Utils::OSDetect->getOS()->sqlHelperClass()->randomFunction(), + 'group_by' => 'me.year', + ); + + if (ref($filteredGenres) eq 'ARRAY' && scalar @$filteredGenres > 0) { + + $cond{'genreTracks.genre'} = $filteredGenres; + $attr{'join'} = ['genreTracks']; + } + + if ( my $library_id = $prefs->get('library') || Slim::Music::VirtualLibraries->getLibraryIdForClient($client) ) { + + $cond{'libraryTracks.library'} = $library_id; + $attr{'join'} ||= []; + push @{$attr{'join'}}, 'libraryTracks'; + + } + + $years = [ Slim::Schema->rs('Track')->search(\%cond, \%attr)->get_column('me.year')->all ]; + } + + my $year = shift @$years; + + $cache->set('rnd_years_' . $client->id, $years, 'never'); + + main::DEBUGLOG && $log->debug("Selected year $year"); + + return $year; +} + +# Add random tracks to playlist if necessary +sub playRandom { + # If addOnly, then track(s) are appended to end. Otherwise, a new playlist is created. + my ($client, $type, $addOnly) = @_; + + $client = $client->master; + + main::DEBUGLOG && $log->debug("Called with type $type"); + + $client->pluginData('type', '') unless $client->pluginData('type'); + + $type ||= 'track'; + $type = lc($type); + + # Whether to keep adding tracks after generating the initial playlist + my $continuousMode = $prefs->get('continuous'); + + if ($type ne $client->pluginData('type')) { + $cache->remove('rnd_idList_' . $client->id); + } + + if (my $idList = $cache->get('dstm_idList_' . $client->id)) { + $cache->set('rnd_idList_' . $client->id, $idList, 'never'); + $cache->remove('dstm_idList_' . $client->id); + } + + my $songIndex = Slim::Player::Source::streamingSongIndex($client); + my $songsRemaining = Slim::Player::Playlist::count($client) - $songIndex - 1; + + main::DEBUGLOG && $log->debug("$songsRemaining songs remaining, songIndex = $songIndex"); + + # Work out how many items need adding + my $numItems = 0; + + if ($type =~ /track|year/) { + + # Add new tracks if there aren't enough after the current track + my $numRandomTracks = $prefs->get('newtracks'); + + if (!$addOnly) { + + $numItems = $numRandomTracks; + + } elsif ($songsRemaining < $numRandomTracks) { + + $numItems = $numRandomTracks - $songsRemaining; + + } else { + + main::DEBUGLOG && $log->debug("$songsRemaining items remaining so not adding new track"); + } + + } elsif ($type ne 'disable' && ($type ne $client->pluginData('type') || !$addOnly || $songsRemaining <= 0)) { + + # Old artist/album/year is finished or new random mix started. Add a new one + $numItems = 1; + } + + if ($numItems) { + + # String to show with showBriefly + my $string = ''; + + if ($type ne 'track') { + $string = $client->string('PLUGIN_RANDOM_' . uc($type) . '_ITEM') . ': '; + } + + # If not track mode, add tracks then go round again to check whether the playlist only + # contains one track (i.e. the artist/album/year only had one track in it). If so, + # add another artist/album/year or the plugin would never add more when the first finished in continuous mode. + for (my $i = 0; $i < 2; $i++) { + + if ($i == 0 || ($type =~ /track|year/ && Slim::Player::Playlist::count($client) == 1 && $continuousMode)) { + + if ($i == 1) { + $string .= ' // '; + } + + # Get the tracks. year is a special case as we do a find for all tracks that match + # the previously selected year + findAndAdd($client, + $type, + $numItems, + # 2nd time round just add tracks to end + $i == 0 ? $addOnly : 1 + ); + } + } + + # Do a show briefly the first time things are added, or every time a new album/artist/year is added + if (!$addOnly || $type ne $client->pluginData('type') || $type !~ /track|year/) { + + if ($type eq 'track') { + $string = $client->string("PLUGIN_RANDOM_TRACK"); + } + + # Don't do showBrieflys if visualiser screensavers are running as the display messes up + if (Slim::Buttons::Common::mode($client) !~ /^SCREENSAVER./) { + + $client->showBriefly( { + jive => undef, + 'line' => [ string($addOnly ? 'ADDING_TO_PLAYLIST' : 'NOW_PLAYING'), $string ] + }, 2, undef, undef, 1); + } + } + + # Never show random as modified, since its a living playlist + $client->currentPlaylistModified(0); + } + + if ($type eq 'disable') { + + main::INFOLOG && $log->info("Cyclic mode ended"); + + # Don't do showBrieflys if visualiser screensavers are running as + # the display messes up + if (Slim::Buttons::Common::mode($client) !~ /^SCREENSAVER./ && !$client->pluginData('disableMix')) { + + $client->showBriefly( { + jive => string('PLUGIN_RANDOM_DISABLED'), + 'line' => [ string('PLUGIN_RANDOMPLAY'), string('PLUGIN_RANDOM_DISABLED') ] + } ); + + } + + $client->pluginData('disableMix', 0); + $client->pluginData('type', ''); + + } else { + + if ( main::INFOLOG && $log->is_info ) { + $log->info(sprintf( + "Playing %s %s mode with %i items", + $continuousMode ? 'continuous' : 'static', $type, Slim::Player::Playlist::count($client) + )); + } + + #BUG 5444: store the status so that users re-visiting the random mix + # will see a continuous mode state. + if ($continuousMode) { + $client->pluginData('type', $type); + } + } +} + +1; \ No newline at end of file diff --git a/Slim/Plugin/RandomPlay/Plugin.pm b/Slim/Plugin/RandomPlay/Plugin.pm index 3101e6f7455..d13c69d5c69 100644 --- a/Slim/Plugin/RandomPlay/Plugin.pm +++ b/Slim/Plugin/RandomPlay/Plugin.pm @@ -6,13 +6,15 @@ package Slim::Plugin::RandomPlay::Plugin; # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2005-2016 Logitech. +# Logitech Media Server Copyright 2005-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. use strict; use base qw(Slim::Plugin::Base); +use Tie::Cache::LRU::Expires; +use URI::Escape qw(uri_escape_utf8 uri_unescape); use Slim::Buttons::Home; use Slim::Music::VirtualLibraries; @@ -25,6 +27,8 @@ use Slim::Utils::Strings qw(string cstring); use Slim::Utils::Prefs; use Slim::Player::Sync; +use Slim::Plugin::RandomPlay::Mixer; + use constant MENU_WEIGHT => 60; # playlist commands that will stop random play @@ -44,13 +48,13 @@ my %mixTypeMap = ( 'contributors' => 'contributor', 'albums' => 'album', 'year' => 'year', - 'artists' => 'contributor', + 'artists' => 'contributor', ); my @mixTypes = ('track', 'contributor', 'album', 'year'); # Genres for each client (don't access this directly - use getGenres()) -my $genres; +tie my %genres, 'Tie::Cache::LRU::Expires', EXPIRES => 86400, ENTRIES => 10; my $functions; my $htmlTemplate = 'plugins/RandomPlay/list.html'; @@ -62,7 +66,7 @@ my $log = Slim::Utils::Log->addLogCategory({ }); my $prefs = preferences('plugin.randomplay'); -my $cache; +my $cache = Slim::Utils::Cache->new(); my $initialized = 0; @@ -74,7 +78,7 @@ $prefs->migrate( 1, sub { if ( !defined $newtracks ) { $newtracks = 10; } - + my $continuous = Slim::Utils::Prefs::OldPrefs->get('plugin_random_keep_adding_tracks'); if ( !defined $continuous ) { $continuous = 1; @@ -84,12 +88,12 @@ $prefs->migrate( 1, sub { if ( !defined $oldtracks ) { $oldtracks = 10; } - + $prefs->set( 'newtracks', $newtracks ); $prefs->set( 'oldtracks', $oldtracks ); $prefs->set( 'continuous', $continuous ); $prefs->set( 'exclude_genres', Slim::Utils::Prefs::OldPrefs->get('plugin_random_exclude_genres') || [] ); - + 1; } ); @@ -99,7 +103,7 @@ $prefs->setChange(sub { # let's verify whether the list actually has changed my $dirty; - + if (scalar @$new != scalar @$old) { $dirty = 1; } @@ -116,17 +120,17 @@ $prefs->setChange(sub { # only wipe player's idList if the genre list has changed _resetCache() if $dirty; - $genres = undef; + %genres = (); }, 'exclude_genres'); $prefs->setChange(\&_resetCache, 'library'); sub _resetCache { return unless $cache; - + foreach ( Slim::Player::Client::clients() ) { $cache->remove('rnd_idList_' . $_->id); - $cache->remove('rnd_years_' . $_->id); + $cache->remove('rnd_years_' . $_->id); } } @@ -137,27 +141,31 @@ sub weight { MENU_WEIGHT } sub initPlugin { my $class = shift; - $genres = undef; - + %genres = (); + # Regenerate the genre map after a rescan. Slim::Control::Request::subscribe(\&_libraryChanged, [['library','rescan'], ['changed','done']]); - + return if $initialized || !Slim::Schema::hasLibrary(); - + $initialized = 1; # create function map if (!$functions) { foreach (keys %mixTypeMap) { my $type = $mixTypeMap{$_}; - $functions->{$_} = sub { playRandom(shift, $type); } + $functions->{$_} = sub { + my $client = $_[0]; + clearClientGenres($client); + Slim::Plugin::RandomPlay::Mixer::playRandom($client, $type); + } } } $class->SUPER::initPlugin(); # set up our subscription - Slim::Control::Request::subscribe(\&commandCallback, + Slim::Control::Request::subscribe(\&commandCallback, [['playlist'], ['newsong', 'delete', @$stopcommands]]); # |requires Client @@ -179,7 +187,7 @@ sub initPlugin { [1, 0, 0, \&genreSelectAllOrNone]); Slim::Control::Request::addDispatch(['randomplayisactive'], [1, 1, 0, \&cliIsActive]); - + Slim::Player::ProtocolHandlers->registerHandler( randomplay => 'Slim::Plugin::RandomPlay::ProtocolHandler' ); @@ -310,6 +318,7 @@ sub initPlugin { id => 'randomplay', node => 'myMusic', isANode => 1, + windowStyle => 'text_list', window => { titleStyle => 'random' }, }, ); @@ -320,7 +329,7 @@ sub initPlugin { after => 'top', func => \&_genreInfoMenu, ) ); - + Slim::Utils::Alarm->addPlaylists('PLUGIN_RANDOMPLAY', [ { title => '{PLUGIN_RANDOM_TRACK}', url => 'randomplay://track' }, @@ -329,99 +338,36 @@ sub initPlugin { { title => '{PLUGIN_RANDOM_YEAR}', url => 'randomplay://year' }, ] ); - - $cache = Slim::Utils::Cache->new(); } sub postinitPlugin { my $class = shift; - + # if user has the Don't Stop The Music plugin enabled, register ourselves if ( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::DontStopTheMusic::Plugin') ) { - require Slim::Plugin::DontStopTheMusic::Plugin; - - my $mixWithGenres = sub { - my ($type, $client, $cb) = @_; - - return unless $client; - - my %genres; - foreach my $track (@{ Slim::Player::Playlist::playList($client) }) { - if ( $track->remote ) { - $genres{$track->genre}++ if $track->genre; - } - else { - foreach ( $track->genres ) { - $genres{$_->name}++ - } - } - } - - # don't seed from radio stations - only do if we're playing from some track based source - if (keys %genres) { - # reset all genres to only use those present in our queue - $client->execute(['randomplaygenreselectall', 0]); - - foreach (keys %genres) { - $client->execute(['randomplaychoosegenre', $_, 1]); - } - } - - $cb->($client, ['randomplay://' . $type]); - }; - - Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_TITLEMIX_WITH_GENRES', sub { - $mixWithGenres->('track', @_); - }); - - Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_TRACK', sub { - my ($client, $cb) = @_; - $client->execute(['randomplaygenreselectall', 0]); - $cb->($client, ['randomplay://track']); - }); - - Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_ALBUM_MIX_WITH_GENRES', sub { - $mixWithGenres->('album', @_); - }); - - Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_ALBUM_ITEM', sub { - my ($client, $cb) = @_; - $client->execute(['randomplaygenreselectall', 0]); - $cb->($client, ['randomplay://album']); - }); - - Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_CONTRIBUTOR_ITEM', sub { - my ($client, $cb) = @_; - $client->execute(['randomplaygenreselectall', 0]); - $cb->($client, ['randomplay://contributor']); - }); - - Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_RANDOM_YEAR_ITEM', sub { - my ($client, $cb) = @_; - $client->execute(['randomplaygenreselectall', 0]); - $cb->($client, ['randomplay://year']); - }); + require Slim::Plugin::RandomPlay::DontStopTheMusic; + Slim::Plugin::RandomPlay::DontStopTheMusic->init(); } } sub _shutdown { - + $initialized = 0; - - $genres = undef; - + + %genres = (); + # unsubscribe Slim::Control::Request::unsubscribe(\&commandCallback); - + # remove Jive menus Slim::Control::Jive::deleteMenuItem('randomplay'); - + # remove player-UI mode Slim::Buttons::Common::setFunction('randomPlay', sub {}); - + # remove web menus webPages(); - + } sub shutdownPlugin { @@ -429,13 +375,13 @@ sub shutdownPlugin { # unsubscribe Slim::Control::Request::unsubscribe(\&_libraryChanged); - - _shutdown(); + + _shutdown(); } sub _libraryChanged { my $request = shift; - + if ( $request->getParam('_newvalue') || $request->isCommand([['rescan'],['done']]) ) { __PACKAGE__->initPlugin(); } else { @@ -446,12 +392,12 @@ sub _libraryChanged { sub _genreInfoMenu { my ($client, $url, $genre, $remoteMeta, $tags) = @_; - + if ($genre) { - my $params = {'genre_id'=> $genre->id}; + my $params = {'genres'=> uri_escape_utf8($genre->name)}; my @items; my $action; - + $action = { command => [ 'randomplay', 'track' ], fixedParams => $params, @@ -463,7 +409,7 @@ sub _genreInfoMenu { }, nextWindow => 'nowPlaying', type => 'play', - name => sprintf('%s %s %s %s', + name => sprintf('%s %s %s %s', cstring($client, 'PLUGIN_RANDOMPLAY'), cstring($client, 'GENRE'), cstring($client, 'SONGS'), @@ -481,13 +427,13 @@ sub _genreInfoMenu { }, nextWindow => 'nowPlaying', type => 'play', - name => sprintf('%s %s %s %s', + name => sprintf('%s %s %s %s', cstring($client, 'PLUGIN_RANDOMPLAY'), cstring($client, 'GENRE'), cstring($client, 'ALBUMS'), $genre->name), }; - + return \@items; } else { @@ -501,14 +447,14 @@ sub _genreInfoMenu { sub genreSelectAllOrNone { my $request = shift; - if (!$initialized) { - $request->setStatusBadConfig(); - return; - } - - my $client = $request->client(); - my $enable = $request->getParam(''); - my $value = $request->getParam('_value'); + if (!$initialized) { + $request->setStatusBadConfig(); + return; + } + + my $client = $request->client(); + my $enable = $request->getParam(''); + my $value = $request->getParam('_value'); my $genres = getGenres($client); my @excluded = (); @@ -527,14 +473,14 @@ sub genreSelectAllOrNone { sub chooseGenre { my $request = shift; - if (!$initialized) { - $request->setStatusBadConfig(); - return; - } - - my $client = $request->client(); - my $genre = $request->getParam('_genre'); - my $value = $request->getParam('_value'); + if (!$initialized) { + $request->setStatusBadConfig(); + return; + } + + my $client = $request->client(); + my $genre = $request->getParam('_genre'); + my $value = $request->getParam('_value'); my $genres = getGenres($client); # in $genres, an enabled genre returns true for $genres->{'enabled'} @@ -544,7 +490,7 @@ sub chooseGenre { for my $genre (keys %$genres) { push @excluded, $genre if $genres->{$genre}->{'enabled'} == 0; } - # set the exclude_genres pref to all disabled genres + # set the exclude_genres pref to all disabled genres $prefs->set('exclude_genres', [@excluded]); $request->setStatusDone(); @@ -554,13 +500,13 @@ sub chooseGenre { sub chooseGenresMenu { my $request = shift; - if (!$initialized) { - $request->setStatusBadConfig(); - return; - } - + if (!$initialized) { + $request->setStatusBadConfig(); + return; + } + my $client = $request->client(); - my $genres = getGenres($client); + my $genres = getGenres($client); my @menu = (); @@ -575,7 +521,7 @@ sub chooseGenresMenu { }, }, }; - + # then a "choose none" item push @menu, { text => $client->string('PLUGIN_RANDOM_SELECT_NONE'), @@ -587,36 +533,36 @@ sub chooseGenresMenu { }, }, }; - + for my $genre ( getSortedGenres($client) ) { my $val = $genres->{$genre}->{'enabled'}; push @menu, { text => $genre, checkbox => ($val == 1) + 0, - actions => { - on => { - player => 0, - cmd => ['randomplaychoosegenre', $genre, 1], - }, - off => { - player => 0, - cmd => ['randomplaychoosegenre', $genre, 0], - }, - }, + actions => { + on => { + player => 0, + cmd => ['randomplaychoosegenre', $genre, 1], + }, + off => { + player => 0, + cmd => ['randomplaychoosegenre', $genre, 0], + }, + }, }; } - + Slim::Control::Jive::sliceAndShip($request, $client, \@menu); } sub chooseLibrary { my $request = shift; - if (!$initialized) { - $request->setStatusBadConfig(); - return; - } + if (!$initialized) { + $request->setStatusBadConfig(); + return; + } $prefs->set('library', $request->getParam('_library') || ''); @@ -627,11 +573,11 @@ sub chooseLibrary { sub chooseLibrariesMenu { my $request = shift; - if (!$initialized) { - $request->setStatusBadConfig(); - return; - } - + if (!$initialized) { + $request->setStatusBadConfig(); + return; + } + my $client = $request->client(); my $library_id = $prefs->get('library'); @@ -647,7 +593,7 @@ sub chooseLibrariesMenu { }, }, }); - + foreach my $id ( sort { lc($libraries->{$a}) cmp lc($libraries->{$b}) } keys %$libraries ) { push @menu, { text => $libraries->{$id}, @@ -660,127 +606,33 @@ sub chooseLibrariesMenu { }, }; } - + Slim::Control::Jive::sliceAndShip($request, $client, \@menu); } -# Find tracks matching parameters and add them to the playlist -sub findAndAdd { - my ($client, $type, $limit, $addOnly) = @_; - - my $idList = $cache->get('rnd_idList_' . $client->id) || []; - - if ( main::INFOLOG && $log->is_info ) { - $log->info(sprintf("Starting random selection of %s items for type: $type", defined($limit) ? $limit : 'unlimited')); - } - - if ( !scalar @$idList ) { - main::DEBUGLOG && $log->debug('Initialize ID list to be randomized'); - - # Search the database for all items of $type which match find criteria - my @joins = (); - - # Initialize find to only include user's selected genres. If they've deselected - # all genres, this clause will be ignored by find, so all genres will be used. - my $filteredGenres = getFilteredGenres($client); - my $excludedGenres = getFilteredGenres($client, 1); - - my %categories = ( - album => ['albums', 0, 999_999, 'tags:t'], - contributor => ['artists', 0, 999_999], - track => ['titles', 0, 999_999, 'tags:t'] - ); - $categories{year} = $categories{track}; - $categories{artist} = $categories{contributor}; - - my $query = $categories{$type}; - - # Only look for genre tracks if we have some, but not all - # genres selected. Or no genres selected. - if ( (scalar @$filteredGenres > 0 && scalar @$excludedGenres != 0) || - scalar @$filteredGenres != 0 && scalar @$excludedGenres > 0 ) { - - push @$query, "genre_id:" . join(',', @$filteredGenres); - - } - - if ( my $library_id = $prefs->get('library') || Slim::Music::VirtualLibraries->getLibraryIdForClient($client) ) { - push @$query, 'library_id:' . $library_id; - } - - if ($type eq 'year') { - push @$query, 'year:' . getRandomYear($client, $filteredGenres); - $type = 'track'; - } - - my $request = Slim::Control::Request::executeRequest($client, $query); - - my $loop = "${type}s_loop"; - $loop = 'artists_loop' if $type eq 'contributor'; - $loop = 'titles_loop' if $type eq 'track'; - - $idList = [ map { $_->{id} } @{ $request->getResult($loop) || [] } ]; - - # shuffle ID list - Slim::Player::Playlist::fischer_yates_shuffle($idList); - } - elsif ($type eq 'year') { - $type = 'track'; - } - - # get first ID from our randomized list - my @randomIds = splice @$idList, 0, $limit; - - $cache->set('rnd_idList_' . $client->id, $idList, 'never'); +# Returns a hash whose keys are the genres in the db +sub getGenres { + my ($client, $useIncludeGenres) = @_; - if (!scalar @randomIds) { + my $includeGenres = $useIncludeGenres && join(':', sort @{$client->pluginData('include_genres') || []}); + my $library_id = $prefs->get('library') || Slim::Music::VirtualLibraries->getLibraryIdForClient($client); - logWarning("Didn't get a valid object for findAndAdd()!"); - return undef; - } + return $genres{$includeGenres} if $genres{$includeGenres}; + return $genres{$library_id} if !$includeGenres && $genres{$library_id}; - # Add the items to the end - foreach my $id (@randomIds) { + my $genreKey = $includeGenres || $library_id; + $genres{$genreKey} ||= {}; - if ( main::INFOLOG && $log->is_info ) { - $log->info(sprintf("%s %s: #%d", - $addOnly ? 'Adding' : 'Playing', $type, $id - )); - } - - # Replace the current playlist with the first item / track or add it to end - my $request = $client->execute([ - 'playlist', $addOnly ? 'addtracks' : 'loadtracks', sprintf('%s.id=%d', $type, $id) - ]); - - # indicate request source - $request->source('PLUGIN_RANDOMPLAY'); - - $addOnly++; - } -} - -# Returns a hash whose keys are the genres in the db -sub getGenres { - my $client = shift; - - my $library_id = $prefs->get('library') || Slim::Music::VirtualLibraries->getLibraryIdForClient($client); - - $genres ||= {}; - - return $genres->{$library_id} if keys %$genres && $genres->{$library_id}; - - $genres->{$library_id} ||= {}; - my $query = ['genres', 0, 999_999]; - + push @$query, 'library_id:' . $library_id if $library_id; my $request = Slim::Control::Request::executeRequest($client, $query); - + # Extract each genre name into a hash my %exclude = map { $_ => 1 } @{ $prefs->get('exclude_genres') }; + my %include = map { $_ => 1 } @{ $client->pluginData('include_genres') } if $includeGenres; my $i = 0; foreach my $genre ( @{ $request->getResult('genres_loop') || [] } ) { @@ -789,35 +641,35 @@ sub getGenres { # Put the name here as well so the hash can be passed to # INPUT.Choice as part of listRef later on - $genres->{$library_id}->{$name} = { + $genres{$genreKey}->{$name} = { 'name' => $name, 'id' => $genre->{id}, - 'enabled' => !$exclude{$name}, + 'enabled' => $includeGenres ? $include{$name} : !$exclude{$name}, 'sort' => $i++, }; } - return $genres->{$library_id}; + return $genres{$genreKey}; } sub getSortedGenres { my $client = shift; - + my $genres = getGenres($client); - return sort { + return sort { $genres->{$a}->{sort} <=> $genres->{$b}->{sort}; } keys %$genres; } # Returns an array of the non-excluded genres in the db sub getFilteredGenres { - my ($client, $returnExcluded, $namesOnly) = @_; - + my ($client, $returnExcluded, $namesOnly, $clientSpecific) = @_; + # use second arg to set what values we return. we may need list of ids or names my $value = $namesOnly ? 'name' : 'id'; - my $genres = getGenres($client); - + my $genres = getGenres($client, $clientSpecific); + return [ map { $genres->{$_}->{$value}; } grep { @@ -825,201 +677,26 @@ sub getFilteredGenres { } keys %$genres ]; } -sub getRandomYear { - my $client = shift; - my $filteredGenres = shift; - - main::DEBUGLOG && $log->debug("Starting random year selection"); - - my $years = $cache->get('rnd_years_' . $client->id) || []; - - if (!scalar @$years) { - my %cond = (); - my %attr = ( - 'order_by' => Slim::Utils::OSDetect->getOS()->sqlHelperClass()->randomFunction(), - 'group_by' => 'me.year', - ); - - if (ref($filteredGenres) eq 'ARRAY' && scalar @$filteredGenres > 0) { - - $cond{'genreTracks.genre'} = $filteredGenres; - $attr{'join'} = ['genreTracks']; - } - - if ( my $library_id = $prefs->get('library') || Slim::Music::VirtualLibraries->getLibraryIdForClient($client) ) { - - $cond{'libraryTracks.library'} = $library_id; - $attr{'join'} ||= []; - push @{$attr{'join'}}, 'libraryTracks'; - - } - - $years = [ Slim::Schema->rs('Track')->search(\%cond, \%attr)->get_column('me.year')->all ]; - } - - my $year = shift @$years; - - $cache->set('rnd_years_' . $client->id, $years, 'never'); - - main::DEBUGLOG && $log->debug("Selected year $year"); - - return $year; -} - -# Add random tracks to playlist if necessary -sub playRandom { - # If addOnly, then track(s) are appended to end. Otherwise, a new playlist is created. - my ($client, $type, $addOnly) = @_; - - $client = $client->master; - - main::DEBUGLOG && $log->debug("Called with type $type"); - - $client->pluginData('type', '') unless $client->pluginData('type'); - - $type ||= 'track'; - $type = lc($type); - - # Whether to keep adding tracks after generating the initial playlist - my $continuousMode = $prefs->get('continuous'); - - if ($type ne $client->pluginData('type')) { - $cache->remove('rnd_idList_' . $client->id); - } - - my $songIndex = Slim::Player::Source::streamingSongIndex($client); - my $songsRemaining = Slim::Player::Playlist::count($client) - $songIndex - 1; - - main::DEBUGLOG && $log->debug("$songsRemaining songs remaining, songIndex = $songIndex"); - - # Work out how many items need adding - my $numItems = 0; - - if ($type =~ /track|year/) { - - # Add new tracks if there aren't enough after the current track - my $numRandomTracks = $prefs->get('newtracks'); - - if (!$addOnly) { - - $numItems = $numRandomTracks; - - } elsif ($songsRemaining < $numRandomTracks) { - - $numItems = $numRandomTracks - $songsRemaining; - - } else { - - main::DEBUGLOG && $log->debug("$songsRemaining items remaining so not adding new track"); - } - - } elsif ($type ne 'disable' && ($type ne $client->pluginData('type') || !$addOnly || $songsRemaining <= 0)) { - - # Old artist/album/year is finished or new random mix started. Add a new one - $numItems = 1; - } - - if ($numItems) { - - # String to show with showBriefly - my $string = ''; - - if ($type ne 'track') { - $string = $client->string('PLUGIN_RANDOM_' . uc($type) . '_ITEM') . ': '; - } - - # If not track mode, add tracks then go round again to check whether the playlist only - # contains one track (i.e. the artist/album/year only had one track in it). If so, - # add another artist/album/year or the plugin would never add more when the first finished in continuous mode. - for (my $i = 0; $i < 2; $i++) { - - if ($i == 0 || ($type =~ /track|year/ && Slim::Player::Playlist::count($client) == 1 && $continuousMode)) { - - if ($i == 1) { - $string .= ' // '; - } - - # Get the tracks. year is a special case as we do a find for all tracks that match - # the previously selected year - findAndAdd($client, - $type, - $numItems, - # 2nd time round just add tracks to end - $i == 0 ? $addOnly : 1 - ); - } - } - - # Do a show briefly the first time things are added, or every time a new album/artist/year is added - if (!$addOnly || $type ne $client->pluginData('type') || $type !~ /track|year/) { - - if ($type eq 'track') { - $string = $client->string("PLUGIN_RANDOM_TRACK"); - } - - # Don't do showBrieflys if visualiser screensavers are running as the display messes up - if (Slim::Buttons::Common::mode($client) !~ /^SCREENSAVER./) { - - $client->showBriefly( { - jive => undef, - 'line' => [ string($addOnly ? 'ADDING_TO_PLAYLIST' : 'NOW_PLAYING'), $string ] - }, 2, undef, undef, 1); - } - } - - # Never show random as modified, since its a living playlist - $client->currentPlaylistModified(0); - } - - if ($type eq 'disable') { - - main::INFOLOG && $log->info("Cyclic mode ended"); - - # Don't do showBrieflys if visualiser screensavers are running as - # the display messes up - if (Slim::Buttons::Common::mode($client) !~ /^SCREENSAVER./ && !$client->pluginData('disableMix')) { - - $client->showBriefly( { - jive => string('PLUGIN_RANDOM_DISABLED'), - 'line' => [ string('PLUGIN_RANDOMPLAY'), string('PLUGIN_RANDOM_DISABLED') ] - } ); - - } - - $client->pluginData('disableMix', 0); - $client->pluginData('type', ''); - - } else { - - if ( main::INFOLOG && $log->is_info ) { - $log->info(sprintf( - "Playing %s %s mode with %i items", - $continuousMode ? 'continuous' : 'static', $type, Slim::Player::Playlist::count($client) - )); - } - - #BUG 5444: store the status so that users re-visiting the random mix - # will see a continuous mode state. - if ($continuousMode) { - $client->pluginData('type', $type); - } - } +sub clearClientGenres { + my $client = $_[0] || return; + $client->pluginData(include_genres => []); + $cache->remove('rnd_idList_' . $client->id); } # Returns the display text for the currently selected item in the menu sub getDisplayText { my ($client, $item) = @_; - + $client = $client->master; - + my $string = 'PLUGIN_RANDOM_' . ($item eq 'genreFilter' ? 'GENRE_FILTER' : uc($item)); - $string =~ s/S$//; + $string =~ s/S$//; # if showing the current mode, show altered string if ($item eq ($client->pluginData('type') || '')) { return string($string . '_PLAYING'); - + # if a mode is active, handle the temporarily added disable option } elsif ($item eq 'disable' && $client->pluginData('type')) { @@ -1041,16 +718,16 @@ sub getOverlay { # Put the right arrow by genre filter and notesymbol by mixes if ($item =~ /^(?:genreFilter|library_filter)$/) { return [undef, $client->symbols('rightarrow')]; - + } elsif (ref $item && ref $item eq 'HASH') { my $value = $item->{value} || ''; my $library_id = $prefs->get('library') || ''; - + return [undef, Slim::Buttons::Common::radioButtonOverlay($client, ($value eq $library_id) ? 1 : 0)]; - + } elsif ($item ne 'disable') { return [undef, $client->symbols('notesymbol')]; - + } else { return [undef, undef]; } @@ -1061,7 +738,7 @@ sub getGenreOverlay { my ($client, $item) = @_; my $rv = 0; - my $genres = getGenres($client); + my $genres = getGenres($client); if ($item->{'selectAll'}) { @@ -1089,9 +766,9 @@ sub getGenreOverlay { # Toggle the exclude state of a genre in the select genres mode sub toggleGenreState { my ($client, $item) = @_; - + my $genres = getGenres($client); - + if ($item->{'selectAll'}) { $item->{'enabled'} = ! $item->{'enabled'}; @@ -1114,9 +791,9 @@ sub toggleGenreState { sub toggleLibrarySelection { my ($client, $item) = @_; - + return unless $item && ref $item; - + $prefs->set('library', $item->{value} || ''); } @@ -1146,7 +823,8 @@ sub handlePlayOrAdd { $client->modeParam('listRef', $listRef); # Go go go! - playRandom($client, $item, $add); + clearClientGenres($client); + Slim::Plugin::RandomPlay::Mixer::playRandom($client, $item, $add); } } @@ -1154,12 +832,12 @@ sub setMode { my $class = shift; my $client = shift; my $method = shift; - + if (!$initialized) { $client->bumpRight(); return; } - + if ($method eq 'pop') { Slim::Buttons::Common::popMode($client); return; @@ -1192,7 +870,7 @@ sub setMode { # Add the genres foreach my $genre ( getSortedGenres($client) ) { - + # HACK: add 'value' so that INPUT.Choice won't complain as much. nasty setup there. $genres->{$genre}->{'value'} = $genres->{$genre}->{'id'}; push @listRef, $genres->{$genre}; @@ -1210,11 +888,11 @@ sub setMode { } elsif ($item eq 'library_filter') { my $library_id = $prefs->get('library'); my $libraries = _getLibraries(); - + my @listRef = ({ name => cstring($client, 'ALL_LIBRARY'), }); - + foreach my $id ( sort { lc($libraries->{$a}) cmp lc($libraries->{$b}) } keys %$libraries ) { push @listRef, { name => $libraries->{$id}, @@ -1261,9 +939,9 @@ sub commandCallback { if (!defined $client || !$client->master->pluginData('type') || !$prefs->get('continuous')) { return; } - + $client = $client->master; - + # Bug 8652, ignore playlist play commands for our randomplay:// URL if ( $request->isCommand( [['playlist'], ['play']] ) ) { my $url = $request->getParam('_item'); @@ -1280,8 +958,8 @@ sub commandCallback { my $songIndex = Slim::Player::Source::streamingSongIndex($client); - if ($request->isCommand([['playlist'], ['newsong']]) || - $request->isCommand([['playlist'], ['delete']]) && + if ($request->isCommand([['playlist'], ['newsong']]) || + $request->isCommand([['playlist'], ['delete']]) && $request->getParam('_index') > $songIndex) { if (main::INFOLOG && $log->is_info) { @@ -1294,7 +972,7 @@ sub commandCallback { } else { $log->info(sprintf("New song detected ($songIndex)")); } - + } else { $log->info(sprintf("Deletion detected (%s)", $request->getParam('_index'))); @@ -1328,7 +1006,7 @@ sub commandCallback { } else { - playRandom($client, $client->pluginData('type'), 1); + Slim::Plugin::RandomPlay::Mixer::playRandom($client, $client->pluginData('type'), 1); } } elsif ($request->isCommand([['playlist'], $stopcommands])) { @@ -1339,26 +1017,26 @@ sub commandCallback { Slim::Utils::Timers::killTimers($client, \&_addTracksLater); - playRandom($client, 'disable'); + Slim::Plugin::RandomPlay::Mixer::playRandom($client, 'disable'); } } sub _addTracksLater { my $client = shift; - + if ($client->pluginData('type')) { - playRandom($client, $client->pluginData('type'), 1); + Slim::Plugin::RandomPlay::Mixer::playRandom($client, $client->pluginData('type'), 1); } } sub cliRequest { my $request = shift; - - if (!$initialized) { - $request->setStatusBadConfig(); - return; - } - + + if (!$initialized) { + $request->setStatusBadConfig(); + return; + } + # get our parameters my $mode = $request->getParam('_mode'); @@ -1366,16 +1044,17 @@ sub cliRequest { $mode = $mixTypeMap{$mode} || $mode; my $client = $request->client(); + clearClientGenres($client); - # return quickly if we lack some information + # return quickly if we lack some information if ($mode && $mode eq 'disable' && $client) { - + # nothing to do here unless a mix is going on if ( !$client->pluginData('type') ) { $request->setStatusDone(); return; } - + $client->pluginData('disableMix', 1); } elsif (!defined $mode || !(scalar grep /$mode/, @mixTypes) || !$client) { @@ -1383,29 +1062,25 @@ sub cliRequest { return; } - if (my $genre = $request->getParam('genre_id')){ - my $name = Slim::Schema->find('Genre', $genre)->name; - - my $genres = getGenres($client); - - # in $genres, an enabled genre returns true for $genres->{'enabled'} - my @excluded = (); - for (keys %$genres) { - push @excluded, $_ unless $_ eq $name; + # if we're called with a list of genres, use these to override the default list + if ( my $genres = $request->getParam('genres') ) { + $genres = uri_unescape($genres); + $client->pluginData(include_genres => [ split(/,/, $genres) ]); + } + elsif (my $genre = $request->getParam('genre_id')){ + if ( my $name = Slim::Schema->find('Genre', $genre)->name ) { + $client->pluginData(include_genres => [ $name ]); } - # set the exclude_genres pref to all disabled genres - $prefs->set('exclude_genres', [@excluded]); - $genres->{$name}->{'enabled'} = 1; } - playRandom($client, $mode); - + Slim::Plugin::RandomPlay::Mixer::playRandom($client, $mode); + $request->setStatusDone(); } sub cliIsActive { my $request = shift; - my $client = $request->client(); + my $client = $request->client(); $request->addResult('_randomplayisactive', active($client) ); $request->setStatusDone(); @@ -1421,7 +1096,8 @@ sub getFunctions { sub buttonStart { my $client = shift; - playRandom($client, $client->pluginData('type') || 'track'); + clearClientGenres($client); + Slim::Plugin::RandomPlay::Mixer::playRandom($client, $client->pluginData('type') || 'track'); } sub webPages { @@ -1463,16 +1139,16 @@ sub handleWebList { $params->{'mixTypes'} = \@mixTypes; $params->{'favorites'} = {}; - map { - $params->{'favorites'}->{$_} = + map { + $params->{'favorites'}->{$_} = Slim::Utils::Favorites->new($client)->findUrl("randomplay://$_") || Slim::Utils::Favorites->new($client)->findUrl("randomplay://$mixTypeMap{$_}") || 0; } keys %mixTypeMap, @mixTypes; - + $params->{'libraries'} ||= _getLibraries(); } - + return Slim::Web::HTTP::filltemplatefile($htmlTemplate, $params); } @@ -1481,7 +1157,8 @@ sub handleWebMix { my ($client, $params) = @_; if (defined $client && $params->{'type'}) { - playRandom($client, $params->{'type'}, $params->{'addOnly'}); + clearClientGenres($client); + Slim::Plugin::RandomPlay::Mixer::playRandom($client, $params->{'type'}, $params->{'addOnly'}); } handleWebList($client, $params); @@ -1502,11 +1179,11 @@ sub handleWebSettings { } $prefs->set('exclude_genres', [keys %{$genres}]); - $prefs->set('newtracks', $params->{'numTracks'}); - $prefs->set('oldtracks', $params->{'numOldTracks'}); + $prefs->set('newtracks', $params->{'numTracks'}); + $prefs->set('oldtracks', $params->{'numOldTracks'}); $prefs->set('continuous', $params->{'continuousMode'} ? 1 : 0); $prefs->set('library', $params->{'useLibrary'}); - + # Pass on to check if the user requested a new mix as well handleWebMix($client, $params); } @@ -1518,13 +1195,13 @@ sub _getLibraries { %libraries = map { $_ => $libraries->{$_}->{name} } keys %$libraries if keys %$libraries; - + return \%libraries; } sub active { my $client = shift; - + return $client->master->pluginData('type'); } diff --git a/Slim/Plugin/RandomPlay/ProtocolHandler.pm b/Slim/Plugin/RandomPlay/ProtocolHandler.pm index 72245473b23..1893757e599 100644 --- a/Slim/Plugin/RandomPlay/ProtocolHandler.pm +++ b/Slim/Plugin/RandomPlay/ProtocolHandler.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RandomPlay::ProtocolHandler; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -11,6 +11,8 @@ package Slim::Plugin::RandomPlay::ProtocolHandler; # GNU General Public License for more details. use strict; +use URI; +use URI::QueryParam; use Slim::Plugin::RandomPlay::Plugin; @@ -19,17 +21,30 @@ sub overridePlayback { return unless $client; - if ($url !~ m|^randomplay://(.*)$|) { - return undef; - } + my $uri = URI->new($url); + + return unless $uri->scheme eq 'randomplay'; if ( Slim::Player::Source::streamingSongIndex($client) ) { # don't start immediately if we're part of a playlist and previous track isn't done playing return if $client->controller()->playingSongDuration() } - $client->execute(["randomplay", "$1"]); - + my ($type) = $url =~ m|^randomplay://([a-z]*)\??|i; + my $params = $uri->query_form_hash; + + my $command = ["randomplay", $type]; + if (my $genres = $params->{genres}) { + push @$command, "genres:$genres"; + } + + $client->execute($command); + + # caller wishes the mix to be a one-off, not to be refreshed + if ($params->{dontContinue}) { + $client->execute(["randomplay", "disable"]); + } + return 1; } @@ -43,16 +58,16 @@ sub isRemote { 0 } sub getMetadataFor { my ( $class, $client, $url ) = @_; - + return unless $client && $url; - + my ($type) = $url =~ m{randomplay://(track|contributor|album|year)s?$}; my $title = 'PLUGIN_RANDOMPLAY'; if ($type) { $title = 'PLUGIN_RANDOM_' . uc($type); } - + return { title => $client->string($title), cover => $class->getIcon(), diff --git a/Slim/Plugin/RandomPlay/strings.txt b/Slim/Plugin/RandomPlay/strings.txt index dd6d29c1e5a..ef6158f7e1c 100644 --- a/Slim/Plugin/RandomPlay/strings.txt +++ b/Slim/Plugin/RandomPlay/strings.txt @@ -1,7 +1,5 @@ # String file for RandomPlay plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_RANDOMPLAY CS Náhodně zvolený mix DA Tilfældigt miks @@ -98,7 +96,7 @@ PLUGIN_RANDOM_TRACK_DISABLE FR ne plus ajouter de morceaux HE הפסק להוסיף שירים IT interrompi aggiunta brani - NL toevoegen van nummers stoppen + NL stop toevoegen van nummers NO slutte å legge til sanger PL zatrzymaj dodawanie utworów RU прекратить добавление песен @@ -161,7 +159,7 @@ PLUGIN_RANDOM_ALBUM_DISABLE FR ne plus ajouter d'albums HE הפסק להוסיף אלבומים IT interrompi aggiunta album - NL toevoegen van albums stoppen + NL stop toevoegen van albums NO slutte å legge til album PL zatrzymaj dodawanie albumów RU прекратить добавление альбомов @@ -224,7 +222,7 @@ PLUGIN_RANDOM_CONTRIBUTOR_DISABLE FR ne plus ajouter d'artistes HE הפסק להוסיף מבצעים IT interrompi aggiunta artisti - NL toevoegen van artiesten stoppen + NL stop toevoegen van artiesten NO slutte å legge til artister PL zatrzymaj dodawanie wykonawców RU прекратить добавление исполнителей @@ -287,7 +285,7 @@ PLUGIN_RANDOM_YEAR_DISABLE FR ne plus ajouter d'années HE הפסק להוסיף שנים IT interrompi aggiunta anni - NL toevoegen van jaren stoppen + NL stop toevoegen van jaren NO slutte å legge til flere år PL zatrzymaj dodawanie lat RU прекратить добавление годов @@ -436,7 +434,7 @@ PLUGIN_RANDOM_GENRE_FILTER_WEB FR Genres à inclure dans le mix: HE סגנונות שיש לכלול במיקס: IT Generi musicali da includere nella raccolta: - NL Genres voor opname in je mix: + NL Genres om toe te voegen aan je mix: NO Sjangere som skal inkluderes i miksen: PL Gatunki do dodania do składanki: RU Жанры, добавляемые в микс: @@ -547,7 +545,7 @@ PLUGIN_RANDOM_PRESS_RIGHT FR Appuyez sur DROITE pour HE לחץ על הלחצן ימינה כדי IT Premere il pulsante DESTRA per - NL Met pijltje-RECHTS kun je + NL Druk RECHTS om NO Gå til høyre for å PL Naciśnij przycisk W PRAWO, aby RU Нажмите RIGHT, чтобы @@ -556,9 +554,11 @@ PLUGIN_RANDOM_PRESS_RIGHT PLUGIN_RANDOM_TITLEMIX_WITH_GENRES DE Titelmix (auf aktuelle Genres beschränkt) EN Song Mix (limited to current genres) + NL Nummermix (alleen binnen huidige genres) NO Sangmiks (begrenset til nåværende sjangre) PLUGIN_RANDOM_ALBUM_MIX_WITH_GENRES DE Zufälliges Album (auf aktuelle Genres beschränkt) EN Random Album (limited to current genres) + NL Willekeurig album (alleen binnen huidige genres) NO Tilfeldig album (begrenset til nåværende sjangre) diff --git a/Slim/Plugin/RemoteLibrary/HTML/EN/plugins/RemoteLibrary/settings.html b/Slim/Plugin/RemoteLibrary/HTML/EN/plugins/RemoteLibrary/settings.html index ce980d0d16d..dbc32a61485 100644 --- a/Slim/Plugin/RemoteLibrary/HTML/EN/plugins/RemoteLibrary/settings.html +++ b/Slim/Plugin/RemoteLibrary/HTML/EN/plugins/RemoteLibrary/settings.html @@ -14,6 +14,20 @@ [% END %] + [% WRAPPER setting title="PLUGIN_REMOTE_LIBRARY_LMS_TRANSCODING" desc="PLUGIN_REMOTE_LIBRARY_LMS_TRANSCODING_DESC" %] + + [% END %] + [% WRAPPER setting title="PLUGIN_REMOTE_LIBRARY_REMOTE_LMS" desc="PLUGIN_REMOTE_LIBRARY_REMOTE_LMS_DESC" %] [% FOREACH server = prefs.pref_remoteLMS %] diff --git a/Slim/Plugin/RemoteLibrary/LMS.pm b/Slim/Plugin/RemoteLibrary/LMS.pm index 366bb5369ec..a251c36c5c6 100644 --- a/Slim/Plugin/RemoteLibrary/LMS.pm +++ b/Slim/Plugin/RemoteLibrary/LMS.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RemoteLibrary::LMS; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -293,11 +293,28 @@ sub proxiedStreamUrl { $id ||= $item->{commonParams}->{track_id} if $item->{commonParams}; my $url = 'lms://' . $remote_library . '/music/' . ($id || 0) . '/download'; + my $suffix; - # XXX - presetParams is only being used by the SlimBrowseProxy. Can be removed in case we're going the BrowseLibrary path - if ($item->{url} || $item->{presetParams}) { - my $suffix = Slim::Music::Info::typeFromSuffix($item->{url} || $item->{presetParams}->{favorites_url} || ''); - $url .= ".$suffix" if $suffix; + # We're using the content type as suffix, though this is not always the correct + # file extension. But we'll need it to be able to correctly transcode if needed. + my $suffix = $item->{ct} || Slim::Music::Info::typeFromSuffix($item->{url}); + + # transcode anything but mp3 if needed + if ( $suffix ne 'mp3' ) { + if ( $prefs->get('transcodeLMS') ) { + $suffix = $prefs->get('transcodeLMS'); + } + elsif ( !main::TRANSCODING ) { + $suffix = 'flac' + } + } + + $url .= ".$suffix"; + + # m4a is difficult: it can be lossless (alac) or lossy (mp4) + # you'll need an up to date remote server for this to work reliably + if ( !$item->{ct} && $suffix eq 'mp4' ) { + $log->error("Streaming m4a/mp4 files from remote source can be problematic. Make sure you're running the latest software on the remote server: $item->{url}"); } return $url; diff --git a/Slim/Plugin/RemoteLibrary/Plugin.pm b/Slim/Plugin/RemoteLibrary/Plugin.pm index c14da15e5a8..49bab590f4b 100644 --- a/Slim/Plugin/RemoteLibrary/Plugin.pm +++ b/Slim/Plugin/RemoteLibrary/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RemoteLibrary::Plugin; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -52,6 +52,7 @@ sub initPlugin { $prefs->init({ useLMS => 1, + transcodeLMS => 'flac', useUPnP => (preferences('server')->get('noupnp') ? 0 : 1), ignoreFolders => sub { my %ignoreItems = Slim::Utils::OSDetect::getOS->ignoredItems(); diff --git a/Slim/Plugin/RemoteLibrary/ProtocolHandler.pm b/Slim/Plugin/RemoteLibrary/ProtocolHandler.pm index b3989297556..81cedd91dd5 100644 --- a/Slim/Plugin/RemoteLibrary/ProtocolHandler.pm +++ b/Slim/Plugin/RemoteLibrary/ProtocolHandler.pm @@ -43,6 +43,23 @@ sub audioScrobblerSource { return 'P'; } +# We use the content type rather than the actual file extension as +# the stream's extension. This helps us to keep backwards compatible +# with a slightly broken /download handler in older server versions. +sub getFormatForURL { + my ($class, $url) = @_; + my $type = 'unk'; + + # test whether the extension is a valid content type + if (defined $url && $url =~ m%^lms:\/\/.*\.([^./]+)$%) { + if ( Slim::Music::Info::isSong(undef, $1) ) { + return lc($1); + } + } + + return $class->SUPER::getFormatForURL($url); +} + # Avoid scanning sub scanUrl { my ($class, $url, $args) = @_; diff --git a/Slim/Plugin/RemoteLibrary/Settings.pm b/Slim/Plugin/RemoteLibrary/Settings.pm index 6ca972256c2..7ef3627d9c4 100644 --- a/Slim/Plugin/RemoteLibrary/Settings.pm +++ b/Slim/Plugin/RemoteLibrary/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RemoteLibrary::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -21,7 +21,7 @@ sub page { } sub prefs { - return ($prefs, qw(useLMS useUPnP ignoreFolders)); + return ($prefs, qw(useLMS useUPnP ignoreFolders transcodeLMS)); } sub handler { diff --git a/Slim/Plugin/RemoteLibrary/UPnP.pm b/Slim/Plugin/RemoteLibrary/UPnP.pm index 1405ca4d2ec..6e9ed75fd00 100644 --- a/Slim/Plugin/RemoteLibrary/UPnP.pm +++ b/Slim/Plugin/RemoteLibrary/UPnP.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RemoteLibrary::UPnP; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/RemoteLibrary/UPnP/ControlPoint.pm b/Slim/Plugin/RemoteLibrary/UPnP/ControlPoint.pm index 398183354a2..d3c8deb3c82 100644 --- a/Slim/Plugin/RemoteLibrary/UPnP/ControlPoint.pm +++ b/Slim/Plugin/RemoteLibrary/UPnP/ControlPoint.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RemoteLibrary::UPnP::ControlPoint; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/RemoteLibrary/UPnP/MediaServer.pm b/Slim/Plugin/RemoteLibrary/UPnP/MediaServer.pm index 525fd0d3a3a..8a789264010 100644 --- a/Slim/Plugin/RemoteLibrary/UPnP/MediaServer.pm +++ b/Slim/Plugin/RemoteLibrary/UPnP/MediaServer.pm @@ -1,6 +1,6 @@ package Slim::Plugin::RemoteLibrary::UPnP::MediaServer; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/RemoteLibrary/install.xml b/Slim/Plugin/RemoteLibrary/install.xml index 4a42a2c4fe9..dd5cb8fb691 100644 --- a/Slim/Plugin/RemoteLibrary/install.xml +++ b/Slim/Plugin/RemoteLibrary/install.xml @@ -12,7 +12,7 @@ plugins/RemoteLibrary/settings.html BROWSE_MUSIC - SlimService + Logitech Media Server 7.9 * diff --git a/Slim/Plugin/RemoteLibrary/strings.txt b/Slim/Plugin/RemoteLibrary/strings.txt index 7196dc83114..234a6f1ec11 100644 --- a/Slim/Plugin/RemoteLibrary/strings.txt +++ b/Slim/Plugin/RemoteLibrary/strings.txt @@ -8,7 +8,7 @@ PLUGIN_REMOTE_LIBRARY_MODULE_NAME PLUGIN_REMOTE_LIBRARY_MODULE_NAME_DESC DE Dieses Plugin erlaubt die Wiedergabe von Musik aus der Sammlung von anderen Logitech Music Server oder UPnP/DLNA Servern, ohne den Player umschalten zu müssen. EN Allow music playback from other Logitech Music Server or UPnP/DLNA servers without switching the player. - NL Deze plug-in maakt het mogelijk muziek af te spelen van een andere Logitech Media Server of UPnP/DLNA-server, zonder van muzieksysteem te wisselen. + NL Deze plug-in maakt het mogelijk muziek af te spelen van een andere Logitech Media Server of UPnP/DLNA-server, zonder van muziekspeler te wisselen. NO Denne modulen gjør det mulig å spille av musikk fra andre Logitech Media Servere eller UPnP/DLNA-servere, uten å måtte koble om spilleren. PL Odtwarzanie muzyki z innych instancji Logitech Media Server lub ze źródeł UPnP/DLNA, bez konieczności przełączania odtwarzacza. @@ -160,6 +160,14 @@ PLUGIN_REMOTE_LIBRARY_LMS_DISABLE NL Uitgeschakeld, niet naar andere Logitech Media Servers zoeken. NO Nei, ikke se etter andre Logitech Media Servere. PL Wyłączone, nie wyszukuj innych instancji Logitech Media Server. + +PLUGIN_REMOTE_LIBRARY_LMS_TRANSCODING + DE Dateikonvertierung + EN File type conversion + +PLUGIN_REMOTE_LIBRARY_LMS_TRANSCODING_DESC + DE Sie können den entfernten Logitech Media Server beauftragen, die Dateien in ein verständliches Format umzuwandeln. + EN You can have the remote Logitech Media Server convert to a format which can be played without further processing. PLUGIN_REMOTE_LIBRARY_SETUP_UPNP CS Klient UPnP/DLNA diff --git a/Slim/Plugin/Rescan/Plugin.pm b/Slim/Plugin/Rescan/Plugin.pm index 34b2bda83e8..f5cf2c1bd77 100644 --- a/Slim/Plugin/Rescan/Plugin.pm +++ b/Slim/Plugin/Rescan/Plugin.pm @@ -2,11 +2,10 @@ package Slim::Plugin::Rescan::Plugin; # Rescan.pm by Andrew Hedges (andrew@hedges.me.uk) October 2002 # Timer functions added by Kevin Deane-Freeman (kevindf@shaw.ca) June 2004 -# $Id: Plugin.pm 11180 2007-01-12 01:04:40Z kdf $ # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -25,6 +24,22 @@ use Scalar::Util qw(blessed); use Slim::Control::Request; use Slim::Utils::Log; use Slim::Utils::Prefs; +use Slim::Utils::Strings qw(cstring); + +use constant RESCAN_TYPES => [ + { + name => '{SETUP_STANDARDRESCAN}', + value => '1rescan', + }, + { + name => '{SETUP_WIPEDB}', + value => '2wipedb', + }, + { + name => '{SETUP_PLAYLISTRESCAN}', + value => '3playlist', + }, +]; my $prefs = preferences('plugin.rescan'); @@ -36,9 +51,7 @@ $prefs->migrate(1, sub { 1; }); -our $interval = 1; # check every x seconds -our @browseMenuChoices; -our %functions; +my $interval = 1; # check every x seconds my @progress = [0]; @@ -48,28 +61,6 @@ sub getDisplayName { sub initPlugin { my $class = shift; - - %functions = ( - 'play' => sub { - my $client = shift; - - if ($client->modeParam('listRef')->[$client->modeParam('listIndex')] eq 'PLUGIN_RESCAN_PRESS_PLAY') { - - executeRescan($client); - - $client->showBriefly( { - 'line' => [ $client->string('PLUGIN_RESCAN_MUSIC_LIBRARY'), - $client->string('PLUGIN_RESCAN_RESCANNING') ] - }); - - Slim::Buttons::Common::pushMode($client, 'scanProgress'); - - } else { - - $client->bumpRight(); - } - } - ); Slim::Buttons::Common::addMode('scanProgress', undef, \&setProgressMode, \&exitProgressMode); @@ -79,9 +70,139 @@ sub initPlugin { Slim::Plugin::Rescan::Settings->new; } + + Slim::Control::Request::addDispatch(['rescanplugin', 'rescan'], [0, 0, 0, \&executeRescan]); + Slim::Control::Request::addDispatch(['rescanplugin', 'menu'], [0, 1, 0, \&jiveRescanMenu]); + Slim::Control::Request::addDispatch(['rescanplugin', 'typemenu'], [0, 1, 0, \&jiveRescanTypeMenu]); + + Slim::Control::Jive::registerPluginMenu([{ + text => $class->getDisplayName, + id => 'settingsRescan', + node => 'advancedSettings', + actions => { + go => { + cmd => ['rescanplugin', 'menu'], + player => 0 + }, + }, + }]); + setTimer(); } +sub jiveRescanMenu { + my $request = shift; + my $client = $request->client(); + + $request->addResult('offset', 0); + + $request->setResultLoopHash('item_loop', 0, { + text => cstring($client, 'PLUGIN_RESCAN_TIMER_SET'), + input => { + initialText => $prefs->get('time'), + title => $client->string('PLUGIN_RESCAN_TIMER_SET'), + _inputStyle => 'time', + len => 1, + help => { + text => $client->string('PLUGIN_RESCAN_TIMER_DESC') + }, + }, + actions => { + do => { + player => 0, + cmd => [ 'pref', 'plugin.rescan:time' ], + params => { + value => '__TAGGEDINPUT__', + enabled => 1, + }, + }, + }, + nextWindow => 'refresh', + }); + + $request->setResultLoopHash('item_loop', 1, { + text => cstring($client, 'PLUGIN_RESCAN_TIMER_NAME'), + checkbox=> $prefs->get('scheduled') ? 1 : 0, + actions => { + on => { + player => 0, + cmd => [ 'pref' , 'plugin.rescan:scheduled', 1 ], + }, + off => { + player => 0, + cmd => [ 'pref' , 'plugin.rescan:scheduled', 0 ], + }, + }, + nextWindow => 'refresh', + }); + + $request->setResultLoopHash('item_loop', 2, { + text => cstring($client, 'PLUGIN_RESCAN_TIMER_TYPE'), + actions => { + go => { + player => 0, + cmd => [ 'rescanplugin' , 'typemenu' ], + }, + }, + }); + + if ( Slim::Music::Import->stillScanning ) { + $request->setResultLoopHash('item_loop', 3, { + text => cstring($client, 'PLUGIN_RESCAN_RESCANNING'), + nextWindow => 'refresh', + }); + } + else { + $request->setResultLoopHash('item_loop', 3, { + text => cstring($client, 'PLUGIN_RESCAN_MUSIC_LIBRARY'), + actions => { + do => { + cmd => [ 'rescanplugin' , 'rescan' ], + }, + }, + nextWindow => 'refresh', + }); + } + + $request->addResult('count', 4); + $request->setStatusDone() +} + +sub jiveRescanTypeMenu { + my $request = shift; + my $client = $request->client(); + + $request->addResult('offset', 0); + + my $i = 0; + my $currentType = $prefs->get('type'); + + foreach ( map { + $_->{name} =~ /\{(.*)\}/; + { + name => $1 || $_->{name}, + value => $_->{value} + } + } @{RESCAN_TYPES()} ) { + $request->setResultLoopHash('item_loop', $i++, { + text => cstring($client, $_->{name}), + radio => $currentType eq $_->{value} ? 1 : 0, + actions => { + do => { + player => 0, + cmd => [ 'pref' , 'plugin.rescan:type' ], + params => { + value => $_->{value} + } + }, + }, + }); + } + + $request->addResult('count', $i); + $request->setStatusDone(); +} + sub setMode { my $class = shift; my $client = shift; @@ -92,12 +213,12 @@ sub setMode { return; } - @browseMenuChoices = ( + my $browseMenuChoices = [ 'PLUGIN_RESCAN_TIMER_SET', 'PLUGIN_RESCAN_TIMER_OFF', 'PLUGIN_RESCAN_TIMER_TYPE', 'PLUGIN_RESCAN_PRESS_PLAY', - ); + ]; if ( Slim::Music::Import->stillScanning ) { @@ -106,11 +227,11 @@ sub setMode { } else { if (Slim::Schema::hasLibrary() && Slim::Schema->rs('Progress')->search( { 'type' => 'importer' }, { 'order_by' => 'start' } )->all) { - push @browseMenuChoices, 'SETUP_VIEW_NOT_SCANNING' + push @$browseMenuChoices, 'SETUP_VIEW_NOT_SCANNING' } my %params = ( - 'listRef' => \@browseMenuChoices, + 'listRef' => $browseMenuChoices, 'externRefArgs' => 'CV', 'header' => 'PLUGIN_RESCAN_MUSIC_LIBRARY', 'headerAddCount' => 1, @@ -381,20 +502,7 @@ sub rescanExitHandler { my $value = $prefs->get('type'); my %params = ( - 'listRef' => [ - { - name => '{SETUP_STANDARDRESCAN}', - value => '1rescan', - }, - { - name => '{SETUP_WIPEDB}', - value => '2wipedb', - }, - { - name => '{SETUP_PLAYLISTRESCAN}', - value => '3playlist', - }, - ], + 'listRef' => RESCAN_TYPES, 'onPlay' => sub { $prefs->set('type', $_[1]->{'value'}); }, 'onAdd' => sub { $prefs->set('type', $_[1]->{'value'}); }, 'onRight' => sub { $prefs->set('type', $_[1]->{'value'}); }, @@ -433,7 +541,27 @@ sub settingsExitHandler { sub getFunctions { my $class = shift; - \%functions; + return { + 'play' => sub { + my $client = shift; + + if ($client->modeParam('listRef')->[$client->modeParam('listIndex')] eq 'PLUGIN_RESCAN_PRESS_PLAY') { + + executeRescan(); + + $client->showBriefly( { + 'line' => [ $client->string('PLUGIN_RESCAN_MUSIC_LIBRARY'), + $client->string('PLUGIN_RESCAN_RESCANNING') ] + }); + + Slim::Buttons::Common::pushMode($client, 'scanProgress'); + + } else { + + $client->bumpRight(); + } + } + }; } sub setTimer { @@ -477,8 +605,6 @@ sub checkScanTimer { } sub executeRescan { - my $client = shift; - my $rescanType = ['rescan']; my $rescanPref = $prefs->get('type') || ''; @@ -495,7 +621,7 @@ sub executeRescan { main::INFOLOG && logger('scan.scanner')->info("Initiating scan of type: ", $rescanType->[0]); - Slim::Control::Request::executeRequest($client, $rescanType); + Slim::Control::Request::executeRequest(undef, $rescanType); } } diff --git a/Slim/Plugin/Rescan/Settings.pm b/Slim/Plugin/Rescan/Settings.pm index ceaebf774d2..e24fbd0c308 100644 --- a/Slim/Plugin/Rescan/Settings.pm +++ b/Slim/Plugin/Rescan/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::Rescan::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/Rescan/strings.txt b/Slim/Plugin/Rescan/strings.txt index 11cb17b05ad..a238a6191d9 100644 --- a/Slim/Plugin/Rescan/strings.txt +++ b/Slim/Plugin/Rescan/strings.txt @@ -1,7 +1,5 @@ # String file for Rescan plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - PLUGIN_RESCAN_MUSIC_LIBRARY CS Znovu prohledat hudební knihovnu DA Gennemsøg musikbibliotek igen @@ -43,7 +41,7 @@ PLUGIN_RESCAN_RESCANNING FR Analyse en cours... HE השרת מבצע סריקה חוזרת... IT Ripetizione analisi in corso... - NL Server is nu bezig met opnieuw scannen... + NL Server scant opnieuw... NO Serveren søker på nytt... PL Serwer przeszukuje ponownie... RU Сервер производит повторное сканирование... diff --git a/Slim/Plugin/RhapsodyDirect/Plugin.pm b/Slim/Plugin/RhapsodyDirect/Plugin.pm index 8f07f5e78a0..0c25869d43a 100644 --- a/Slim/Plugin/RhapsodyDirect/Plugin.pm +++ b/Slim/Plugin/RhapsodyDirect/Plugin.pm @@ -6,7 +6,6 @@ use strict; use base 'Slim::Plugin::OPMLBased'; use Slim::Networking::SqueezeNetwork; -use Slim::Plugin::RhapsodyDirect::ProtocolHandler; use URI::Escape qw(uri_escape_utf8); @@ -18,13 +17,36 @@ my $log = Slim::Utils::Log->addLogCategory({ sub initPlugin { my $class = shift; - + + if ( !Slim::Networking::Async::HTTP->hasSSL() ) { + $log->error(Slim::Utils::Strings::string('SERVICE_REQUIRES_HTTPS')); + + return $class->SUPER::initPlugin( + feed => sub { + my ($client, $cb, $args) = @_; + + $cb->({ + items => [{ + name => Slim::Utils::Strings::cstring($client, 'SERVICE_REQUIRES_HTTPS'), + type => 'textarea' + }], + }); + }, + tag => 'rhapsodydirect', + menu => 'music_services', + is_app => 1, + weight => 20, + ); + } + + require Slim::Plugin::RhapsodyDirect::ProtocolHandler; + Slim::Player::ProtocolHandlers->registerHandler( rhapd => 'Slim::Plugin::RhapsodyDirect::ProtocolHandler' ); Slim::Player::ProtocolHandlers->registerIconHandler( - qr|squeezenetwork\.com.*/api/rhapsody/|, + qr|mysqueezebox\.com.*/api/rhapsody/|, sub { Slim::Plugin::RhapsodyDirect::ProtocolHandler->getIcon(); } ); diff --git a/Slim/Plugin/RhapsodyDirect/ProtocolHandler.pm b/Slim/Plugin/RhapsodyDirect/ProtocolHandler.pm index e129922cf50..31eecaab650 100644 --- a/Slim/Plugin/RhapsodyDirect/ProtocolHandler.pm +++ b/Slim/Plugin/RhapsodyDirect/ProtocolHandler.pm @@ -3,7 +3,7 @@ package Slim::Plugin::RhapsodyDirect::ProtocolHandler; # Rhapsody Direct / Napster handler for rhapd:// URLs. use strict; -use base qw(Slim::Player::Protocols::HTTP); +use base qw(Slim::Player::Protocols::HTTPS); use HTML::Entities qw(encode_entities); use JSON::XS::VersionOneAndTwo; diff --git a/Slim/Plugin/RhapsodyDirect/strings.txt b/Slim/Plugin/RhapsodyDirect/strings.txt index a5cccb4ba04..615dc35325a 100644 --- a/Slim/Plugin/RhapsodyDirect/strings.txt +++ b/Slim/Plugin/RhapsodyDirect/strings.txt @@ -73,7 +73,7 @@ PLUGIN_RHAPSODY_DIRECT_RPDS_INVALID FI Virheellinen Napster vastaus soittimelta. FR Réponse Napster non valide de la part de la platine. IT Risposta Napster non valida dal lettore. - NL Ongeldige Napster-respons van muzieksysteem. + NL Ongeldige Napster-respons van muziekspeler. NO Ugyldig Napster-respons fra spilleren. PL Nieprawidłowa odpowiedź dla usługi Napster z odtwarzacza. RU Недействительный ответ Napster от плеера. @@ -103,7 +103,7 @@ PLUGIN_RHAPSODY_DIRECT_RPDS_SSL_ERROR FI Soitin ei voinut kommunikoida Napster kanssa. FR La platine n'a pas pu communiquer avec Napster. IT Il lettore non è stato in grado di comunicare con Napster. - NL Muzieksysteem kan niet met Napster communiceren. + NL Muziekspeler kan niet met Napster communiceren. NO Spilleren kunne ikke kommunisere med Napster. PL Odtwarzacz nie mógł połączyć się z usługą Napster. RU Плееру не удалось обменяться данными с Napster. @@ -118,7 +118,7 @@ PLUGIN_RHAPSODY_DIRECT_PLAYER_REQUIRED FI Tämä soitin ei tue Napsteria. Vaaditaan Squeezebox2 tai uudempi. FR Cette platine ne prend pas en charge Napster. Vous devez disposer de Squeezebox2 ou d'une version ultérieure. IT Questo lettore non supporta Napster. È necessario utilizzare Squeezebox2 o versione successiva. - NL Dit muzieksysteem biedt geen ondersteuning voor Napster. Een Squeezebox2 of hoger is vereist. + NL Deze muziekspeler biedt geen ondersteuning voor Napster. Een Squeezebox2 of hoger is vereist. NO Denne spilleren støtter ikke Napster. En Squeezebox2 eller høyere er påkrevd. PL Ten odtwarzacz nie obsługuje usługi Napster. Wymagane jest urządzenie Squeezebox2 lub nowsze. RU Этот плеер не поддерживает Napster. Требуется Squeezebox2 или более высокой версии. @@ -253,7 +253,7 @@ PLUGIN_RHAPSODY_DIRECT_TOO_MANY_SYNCED FI Napster ei toimi, jos näin monta soitinta on synkronoitu. Poista yhden tai useamman soittimen synkronointi. FR Napster ne peut fonctionner avec autant de platines synchronisées. Veuillez désynchroniser au moins une platine. IT Impossibile utilizzare Napster con un numero così elevato di lettori sincronizzati. Annullare la sincronizzazione di uno o più lettori. - NL Napster kan niet spelen wanneer er zoveel muzieksystemen gesynchroniseerd zijn. Hef de synchronisatie van een of meer systemen op. + NL Napster kan niet spelen wanneer er zoveel muziekspelers gesynchroniseerd zijn. Hef de synchronisatie van een of meer systemen op. NO Napster kan ikke brukes med så mange synkroniserte spillere. Du må fjerne én eller flere spillere fra synkroniseringen. PL Odtwarzanie przez usługę Napster przy takiej liczbie zsynchronizowanych odtwarzaczy jest niemożliwe. Wyłącz synchronizację co najmniej jednego odtwarzacza. RU Napster не может воспроизводить, когда синхронизируется так много плееров. Отмените синхронизацию для одного или нескольких плееров. diff --git a/Slim/Plugin/SN/Plugin.pm b/Slim/Plugin/SN/Plugin.pm index d60594e0836..6b8edeec251 100644 --- a/Slim/Plugin/SN/Plugin.pm +++ b/Slim/Plugin/SN/Plugin.pm @@ -1,8 +1,7 @@ package Slim::Plugin::SN::Plugin; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech +# Logitech Media Server Copyright 2001-2020 Logitech # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/SN/ProtocolHandler.pm b/Slim/Plugin/SN/ProtocolHandler.pm index e522a423f8f..f250688523c 100644 --- a/Slim/Plugin/SN/ProtocolHandler.pm +++ b/Slim/Plugin/SN/ProtocolHandler.pm @@ -1,8 +1,6 @@ package Slim::Plugin::SN::ProtocolHandler; -# $Id - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/SN/strings.txt b/Slim/Plugin/SN/strings.txt index 945b9f90c2b..7f8b0d09e80 100644 --- a/Slim/Plugin/SN/strings.txt +++ b/Slim/Plugin/SN/strings.txt @@ -1,7 +1,5 @@ # String file for SN plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z $ - PLUGIN_SN CS Obslužný program služeb mysqueezebox.com DA mysqueezebox.com – tjenestehåndtering diff --git a/Slim/Plugin/SavePlaylist/Plugin.pm b/Slim/Plugin/SavePlaylist/Plugin.pm index de8d3d8f4ea..e41f6fcbe52 100644 --- a/Slim/Plugin/SavePlaylist/Plugin.pm +++ b/Slim/Plugin/SavePlaylist/Plugin.pm @@ -1,9 +1,8 @@ package Slim::Plugin::SavePlaylist::Plugin; -# $Id: Plugin.pm 11071 2007-01-01 15:47:59Z adrian $ # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/SavePlaylist/strings.txt b/Slim/Plugin/SavePlaylist/strings.txt index 6abbc6b6808..076be8d0971 100644 --- a/Slim/Plugin/SavePlaylist/strings.txt +++ b/Slim/Plugin/SavePlaylist/strings.txt @@ -1,7 +1,5 @@ # String file for SavePlaylist plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - SAVE_PLAYLIST_DESC CS Uložit seznamy skladeb z UI přehrávače DA Gem afspilningslister fra afspillerens brugerflade @@ -11,7 +9,7 @@ SAVE_PLAYLIST_DESC FI Tallenna soittoluettelot soittimen käyttöliittymästä FR Enregistrer des listes de lecture à partir de l'interface de la platine IT Salva playlist dall'interfaccia del lettore - NL Playlists uit de muzieksysteem-UI opslaan + NL Playlists uit de muziekspeler-UI opslaan NO Lagre spillelister fra spillergrensesnittet PL Zapisz listy odtwarzania z interfejsu odtwarzacza RU Сохранить плей-листы из пользовательского интерфейса плеера @@ -27,7 +25,7 @@ PLAYLIST_OVERWRITE FR Appuyez sur DROITE pour remplacer la liste de lecture existante: HE עבור ימינה להחלפת רשימת ההשמעה הנוכחית: IT Premere DESTRA per sostituire la playlist corrente: - NL Rechts om de bestaande playlist te vervangen: + NL Druk RECHTS om de bestaande playlist te vervangen: NO Gå til høyre for å bytte ut den eksisterende spillelisten: PL Przejdź w prawo, aby zastąpić istniejącą listę odtwarzania: RU Перейдите вправо, чтобы заменить существующий плей-лист: @@ -44,7 +42,7 @@ PLAYLIST_SAVE HE עבור ימינה פעם נוספת לשמירת רשימת ההשמעה בשם: IT Premere DESTRA nuovamente per salvare la playlist con nome: JA プレイリストを保存するにはRIGHTをもう一度押してください : - NL Opnieuw rechts om playlist op te slaan als: + NL Druk opnieuw RECHTS om playlist op te slaan als: NO Gå til høyre igjen for å lagre spillelisten som: PL Ponownie w prawo, aby zapisać listę odtwarzania jako: RU Снова вправо, чтобы сохранить плей-лист как: diff --git a/Slim/Plugin/Slacker/Plugin.pm b/Slim/Plugin/Slacker/Plugin.pm index bd39b36527a..9454207e89a 100644 --- a/Slim/Plugin/Slacker/Plugin.pm +++ b/Slim/Plugin/Slacker/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Slacker::Plugin; -# $Id$ use strict; use base qw(Slim::Plugin::OPMLBased); @@ -24,7 +23,7 @@ sub initPlugin { ); Slim::Player::ProtocolHandlers->registerIconHandler( - qr|squeezenetwork\.com.*/api/slacker/|, + qr|mysqueezebox\.com.*/api/slacker/|, sub { Slim::Plugin::Slacker::Plugin->_pluginDataFor('icon'); } ); diff --git a/Slim/Plugin/Slacker/ProtocolHandler.pm b/Slim/Plugin/Slacker/ProtocolHandler.pm index 12445c562d1..d5493959988 100644 --- a/Slim/Plugin/Slacker/ProtocolHandler.pm +++ b/Slim/Plugin/Slacker/ProtocolHandler.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Slacker::ProtocolHandler; -# $Id$ use strict; use base qw(Slim::Player::Protocols::HTTP); diff --git a/Slim/Plugin/SlimTris/Plugin.pm b/Slim/Plugin/SlimTris/Plugin.pm index c4b2e814ad6..749bd19be2b 100644 --- a/Slim/Plugin/SlimTris/Plugin.pm +++ b/Slim/Plugin/SlimTris/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::SlimTris::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/SlimTris/strings.txt b/Slim/Plugin/SlimTris/strings.txt index 893f1f6da3a..d60fd260be6 100644 --- a/Slim/Plugin/SlimTris/strings.txt +++ b/Slim/Plugin/SlimTris/strings.txt @@ -1,7 +1,5 @@ # String file for SlimTris plugin -# $Id: strings.txt 11037 2006-12-24 07:25:38Z mherger $ - SLIMTRIS CS SlimTris DA SlimTris diff --git a/Slim/Plugin/Snow/Plugin.pm b/Slim/Plugin/Snow/Plugin.pm index bcaa4be67fa..5a18b18374f 100644 --- a/Slim/Plugin/Snow/Plugin.pm +++ b/Slim/Plugin/Snow/Plugin.pm @@ -1,13 +1,12 @@ package Slim::Plugin::Snow::Plugin; -# $Id: Plugin.pm 11021 2006-12-21 22:28:39Z dsully $ # by Phil Barrett, December 2003 # screensaver conversion by Kevin Deane-Freeman Dec 2003 # graphic SB code added by James Craig September 2005 # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/Snow/strings.txt b/Slim/Plugin/Snow/strings.txt index 34fc6826230..f1429564415 100644 --- a/Slim/Plugin/Snow/strings.txt +++ b/Slim/Plugin/Snow/strings.txt @@ -1,7 +1,5 @@ # String file for Snow plugin -# $Id: strings.txt 11049 2006-12-28 09:47:38Z mherger $ - PLUGIN_SCREENSAVER_SNOW CS Spořič obrazovky s motivem sněhu DA Pauseskærm: diff --git a/Slim/Plugin/SongScanner/Plugin.pm b/Slim/Plugin/SongScanner/Plugin.pm index 4971d85a377..4281176eecd 100644 --- a/Slim/Plugin/SongScanner/Plugin.pm +++ b/Slim/Plugin/SongScanner/Plugin.pm @@ -15,7 +15,7 @@ use base qw(Slim::Plugin::Base); # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2001-2011 Logitech +# Logitech Media Server Copyright 2001-2020 Logitech # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/SongScanner/strings.txt b/Slim/Plugin/SongScanner/strings.txt index 5d83c85eaaf..ad84d1307b7 100644 --- a/Slim/Plugin/SongScanner/strings.txt +++ b/Slim/Plugin/SongScanner/strings.txt @@ -1,7 +1,5 @@ # String file for SongScanner plugin -# $Id: strings.txt,v 1.1 2007-11-07 07:38:15 fishbone Exp $ - PLUGIN_SONGSCANNER CS Prohlížeč skladby DA Analyse af nummer @@ -26,7 +24,7 @@ PLUGIN_SONGSCANNER_DESC FI Soittimen käyttöliittymä satunnaisiin paikkoihin hyppäämiseen toistetun kappaleen sisällä (Squeezebox Classic, Squeezebox Boom, Transporter) FR Interface de la platine permettant d'accéder à des emplacements arbitraires du morceau en cours de lecture (Squeezebox Classic, Squeezebox Boom, Transporter) IT Interfaccia lettore per passare a posizioni casuali all'interno del brano in corso di riproduzione (Squeezebox Classic, Squeezebox Boom, Transporter) - NL Muzieksysteeminterface om naar willekeurige posities in het huidige nummer te springen (Squeezebox Classic, Squeezebox Boom, Transporter) + NL Muziekspelerinterface om naar willekeurige posities in het huidige nummer te springen (Squeezebox Classic, Squeezebox Boom, Transporter) NO Spillergrensesnitt for å gå til vilkårlige plasseringer i sangen som spilles av (Squeezebox Classic, Squeezebox Boom, Transporter) PL Interfejs odtwarzacza umożliwiający przejście do dowolnego miejsca w aktualnie odtwarzanym utworze (Squeezebox Classic, Squeezebox Boom, Transporter) RU Интерфейс плеера для перехода в произвольную позицию воспроизводимой песни (Squeezebox Classic, Squeezebox Boom, Transporter) diff --git a/Slim/Plugin/Sounds/Plugin.pm b/Slim/Plugin/Sounds/Plugin.pm index efc3eae5a6c..8e34ab77573 100644 --- a/Slim/Plugin/Sounds/Plugin.pm +++ b/Slim/Plugin/Sounds/Plugin.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Sounds::Plugin; -# $Id$ # Browse Sounds & Effects diff --git a/Slim/Plugin/Sounds/ProtocolHandler.pm b/Slim/Plugin/Sounds/ProtocolHandler.pm index f2c650e5fd6..d2397e07b5c 100644 --- a/Slim/Plugin/Sounds/ProtocolHandler.pm +++ b/Slim/Plugin/Sounds/ProtocolHandler.pm @@ -1,6 +1,5 @@ package Slim::Plugin::Sounds::ProtocolHandler; -# $Id$ # Handler for forcing loop mode diff --git a/Slim/Plugin/SpotifyLogi/HTML/EN/plugins/SpotifyLogi/html/images/icon.png b/Slim/Plugin/SpotifyLogi/HTML/EN/plugins/SpotifyLogi/html/images/icon.png deleted file mode 100644 index 2ad485f7ec2..00000000000 Binary files a/Slim/Plugin/SpotifyLogi/HTML/EN/plugins/SpotifyLogi/html/images/icon.png and /dev/null differ diff --git a/Slim/Plugin/SpotifyLogi/Plugin.pm b/Slim/Plugin/SpotifyLogi/Plugin.pm deleted file mode 100644 index c996f301467..00000000000 --- a/Slim/Plugin/SpotifyLogi/Plugin.pm +++ /dev/null @@ -1,351 +0,0 @@ -package Slim::Plugin::SpotifyLogi::Plugin; - -# Logitech Media Server Copyright 2001-2016 Logitech. -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, -# version 2. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -use strict; -use base 'Slim::Plugin::OPMLBased'; - -use Slim::Networking::SqueezeNetwork; -use Slim::Plugin::SpotifyLogi::ProtocolHandler; - -use JSON::XS::VersionOneAndTwo; -use URI::Escape qw(uri_escape_utf8); - -my $log = Slim::Utils::Log->addLogCategory( { - 'category' => 'plugin.spotifylogi', - 'defaultLevel' => 'ERROR', - 'description' => 'PLUGIN_SPOTIFYLOGI_MODULE_NAME', -} ); - -=pod -typedef enum sp_error { - SP_ERROR_OK = 0, ///< No errors encountered - SP_ERROR_BAD_API_VERSION = 1, ///< The library version targeted does not match the one you claim you support - SP_ERROR_API_INITIALIZATION_FAILED = 2, ///< Initialization of library failed - are cache locations etc. valid? - SP_ERROR_TRACK_NOT_PLAYABLE = 3, ///< The track specified for playing cannot be played - SP_ERROR_RESOURCE_NOT_LOADED = 4, ///< One or several of the supplied resources is not yet loaded - SP_ERROR_BAD_APPLICATION_KEY = 5, ///< The application key is invalid - SP_ERROR_BAD_USERNAME_OR_PASSWORD = 6, ///< Login failed because of bad username and/or password - SP_ERROR_USER_BANNED = 7, ///< The specified username is banned - SP_ERROR_UNABLE_TO_CONTACT_SERVER = 8, ///< Cannot connect to the Spotify backend system - SP_ERROR_CLIENT_TOO_OLD = 9, ///< Client is too old, library will need to be updated - SP_ERROR_OTHER_PERMANENT = 10, ///< Some other error occured, and it is permanent (e.g. trying to relogin will not help) - SP_ERROR_BAD_USER_AGENT = 11, ///< The user agent string is invalid or too long - SP_ERROR_MISSING_CALLBACK = 12, ///< No valid callback registered to handle events - SP_ERROR_INVALID_INDATA = 13, ///< Input data was either missing or invalid - SP_ERROR_INDEX_OUT_OF_RANGE = 14, ///< Index out of range - SP_ERROR_USER_NEEDS_PREMIUM = 15, ///< The specified user needs a premium account - SP_ERROR_OTHER_TRANSIENT = 16, ///< A transient error occured. - SP_ERROR_IS_LOADING = 17, ///< The resource is currently loading - SP_ERROR_NO_STREAM_AVAILABLE = 18, ///< Could not find any suitable stream to play - SP_ERROR_PERMISSION_DENIED = 19, ///< Requested operation is not allowed - SP_ERROR_INBOX_IS_FULL = 20, ///< Target inbox is full -} sp_error; - -100+ are internal errors: - -100 - No Spotify URI found for playback -101 - Not a Spotify track URI -102 - Spotify play token lost, account in use elsewhere -103 - Track is not available for playback (sp_track_is_available returns false) -=cut - -# Stop playback on these errors. Other errors will skip -# to the next track. -my @stop_errors = ( - 4, # SP_ERROR_RESOURCE_NOT_LOADED - 5, # SP_ERROR_BAD_APPLICATION_KEY - 6, # SP_ERROR_BAD_USERNAME_OR_PASSWORD - 7, # SP_ERROR_USER_BANNED - 8, # SP_ERROR_UNABLE_TO_CONTACT_SERVER - 9, # SP_ERROR_CLIENT_TOO_OLD, - 10, # SP_ERROR_OTHER_PERMANENT - 15, # SP_ERROR_USER_NEEDS_PREMIUM - 19, # SP_ERROR_PERMISSION_DENIED, - 102, # Spotify play token lost, account in use elsewhere -); - -# Report these errors -my @report_errors = ( - 6, # SP_ERROR_BAD_USERNAME_OR_PASSWORD -); - -sub initPlugin { - my $class = shift; - - Slim::Player::ProtocolHandlers->registerHandler( - spotify => 'Slim::Plugin::SpotifyLogi::ProtocolHandler' - ); - - # in LMS we're going to authenticate every player - don't do this on mysb, it's too expensive - Slim::Plugin::SpotifyLogi::ProtocolHandler->init(); - - Slim::Player::ProtocolHandlers->registerIconHandler( - qr|squeezenetwork\.com.*/api/spotify/|, - sub { Slim::Plugin::SpotifyLogi::ProtocolHandler->getIcon(); } - ); - - Slim::Networking::Slimproto::addHandler( - SPDS => \&spds_handler - ); - - # Track Info item - Slim::Menu::TrackInfo->registerInfoProvider( spotifylogi => ( - after => 'middle', - func => \&trackInfoMenu, - ) ); - - # Commands init - Slim::Control::Request::addDispatch(['spotify', 'star', '_uri'], - [0, 1, 1, \&star]); - - $class->SUPER::initPlugin( - feed => Slim::Networking::SqueezeNetwork->url('/api/spotify/v1/opml'), - tag => 'spotifylogi', - menu => 'music_services', - weight => 20, - is_app => 1, - ); - - if ( main::WEBUI ) { - # Add a function to view trackinfo in the web - Slim::Web::Pages->addPageFunction( - 'plugins/spotifylogi/trackinfo.html', - sub { - my $client = $_[0]; - my $params = $_[1]; - - my $url; - - my $id = $params->{sess} || $params->{item}; - - if ( $id ) { - # The user clicked on a different URL than is currently playing - if ( my $track = Slim::Schema->find( Track => $id ) ) { - $url = $track->url; - } - - # Pass-through track ID as sess param - $params->{sess} = $id; - } - else { - $url = Slim::Player::Playlist::url($client); - } - - Slim::Web::XMLBrowser->handleWebIndex( { - client => $client, - feed => Slim::Plugin::SpotifyLogi::ProtocolHandler->trackInfoURL( $client, $url ), - path => 'plugins/spotifylogi/trackinfo.html', - title => 'Spotify Track Info', - timeout => 35, - args => \@_ - } ); - }, - ); - } -} - -sub postinitPlugin { - my $class = shift; - - # if user has the Don't Stop The Music plugin enabled, register ourselves - if ( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::DontStopTheMusic::Plugin') ) { - require Slim::Plugin::DontStopTheMusic::Plugin; - Slim::Plugin::DontStopTheMusic::Plugin->registerHandler('PLUGIN_SPOTIFYLOGI_RECOMMENDATIONS', \&dontStopTheMusic); - } -} - -sub dontStopTheMusic { - my ($client, $cb) = @_; - - my $seedTracks = Slim::Plugin::DontStopTheMusic::Plugin->getMixableProperties($client, 5); - - # don't seed from radio stations - only do if we're playing from some track based source - if ($seedTracks && ref $seedTracks && scalar @$seedTracks) { - main::INFOLOG && $log->info("Auto-mixing Spotify tracks from random items in current playlist"); - - my $http = Slim::Networking::SqueezeNetwork->new( - sub { - my $http = shift; - my $client = $http->params->{client}; - - my $content = eval { from_json( $http->content ) }; - my @tracks; - - if ( $@ || ($content && $content->{error}) ) { - $http->error( $@ || $content->{error} ); - } - elsif ( $content && ref $content && $content->{body} && (my $items = $content->{body}->{outline}) ) { - @tracks = grep { $_ } map { $_->{play} } @$items; - } - - $cb->($client, \@tracks); - }, - sub { - my $http = shift; - my $client = $http->params->{client}; - - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( 'getMix failed: ' . $http->error ); - } - - $cb->($client); - }, - { - client => $client, - timeout => 15, - }, - ); - - my $json = eval { to_json({ - seed => $seedTracks - }) }; - - if ( $@ ) { - $log->error("JSON encoding failes: $@"); - $json = ''; - } - - $http->post( $http->url( '/api/spotify/v1/opml/autoDJ' ), $json ); - } - else { - $cb->($client); - } -} - -sub getDisplayName () { - return 'PLUGIN_SPOTIFYLOGI_MODULE_NAME'; -} - -# Don't add this item to any menu -sub playerMenu { } - -sub handleError { - my ( $error, $client ) = @_; - - main::DEBUGLOG && $log->debug("Error during request: $error"); -} - -sub trackInfoMenu { - my ( $client, $url, $track, $remoteMeta ) = @_; - - return unless $client; - - # Only show if in the app list - return unless $client->isAppEnabled('spotify'); - - my $artist = $track->remote ? $remoteMeta->{artist} : $track->artistName; - my $album = $track->remote ? $remoteMeta->{album} : ( $track->album ? $track->album->name : undef ); - my $title = $track->remote ? $remoteMeta->{title} : $track->title; - - my $snURL = Slim::Networking::SqueezeNetwork->url( - '/api/spotify/v1/opml/context?artist=' - . uri_escape_utf8($artist) - . '&album=' - . uri_escape_utf8($album) - . '&track=' - . uri_escape_utf8($title) - ); - - if ( $artist && ( $album || $title ) ) { - return { - type => 'link', - name => $client->string('PLUGIN_SPOTIFYLOGI_ON_SPOTIFY'), - url => $snURL, - favorites => 0, - }; - } -} - -sub star { - my $request = shift; - my $client = $request->client(); - my $uri = $request->getParam('_uri'); - - return unless defined $client && $uri; - - main::DEBUGLOG && $log->is_debug && $log->debug("Sending star command to player for $uri"); - - my $data = pack( - 'cC/a*', - 2, - $uri, - ); - - $client->sendFrame( spds => \$data ); - - $request->setStatusDone(); -} - -sub spds_handler { - my ( $client, $data_ref ) = @_; - - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( $client->id . " Got SPDS packet: " . Data::Dump::dump($data_ref) ); - } - - my $got_cmd = unpack 'C', $$data_ref; - - # Check for specific decoding error codes - if ( $got_cmd == 255 ) { - my (undef, $error_code, $message, $spotify_error ) = unpack 'CCC/a*C/a*', $$data_ref; - - if ( $spotify_error ) { - $message .= ': ' . $spotify_error; - } - - $log->error( $client->id . " Spotify error, code $error_code: $message" ); - - my $string = ($error_code >= 3 && $error_code <= 103) ? $client->string("SPOTIFY_ERROR_${error_code}") : $message; - $client->controller()->playerStreamingFailed($client, $string, ' '); # empty string to hide track URL - - my $song = $client->controller->streamingSong(); - Slim::Control::Request::notifyFromArray( $client, [ 'playlist', 'cant_open', $song ? $song->currentTrack()->url : 'unk', "$error_code: $string" ] ); - - # if we fail playback lack of authentication, check whether we can authorize at all - if ( $error_code == 15 ) { - return if Slim::Plugin::SpotifyLogi::ProtocolHandler->initAuthorization($client); - } - - # Report some serious issues to back-end - if ( grep { $error_code == $_ } @report_errors ) { - my $auth = $client->pluginData('info') && $client->pluginData('info')->{auth}; - $auth ||= {}; - - my $http = Slim::Networking::SqueezeNetwork->new( - sub {}, - sub {}, - { client => $client } - )->get( - Slim::Networking::SqueezeNetwork->url( - sprintf('/api/spotify/v1/opml/report_error?account=%s&error=%s', uri_escape_utf8($auth->{username}), $error_code), - ) - ); - } - - # Force stop on certain errors - if ( grep { $error_code == $_ } @stop_errors ) { - # XXX need a better way to stop playback with an error message, calling - # this after playerStreamingFailed is wrong because the next - # track is fetched - Slim::Player::Source::playmode($client, 'stop'); - } - - return; - } - else { - # Older firmware, etc, just fail on any unexpected SPDS - $client->controller()->playerStreamingFailed($client, "Unexpected Spotify error, please update your firmware.", ' '); - } - -} - -1; diff --git a/Slim/Plugin/SpotifyLogi/ProtocolHandler.pm b/Slim/Plugin/SpotifyLogi/ProtocolHandler.pm deleted file mode 100644 index f3bd8108645..00000000000 --- a/Slim/Plugin/SpotifyLogi/ProtocolHandler.pm +++ /dev/null @@ -1,368 +0,0 @@ -package Slim::Plugin::SpotifyLogi::ProtocolHandler; - -use strict; -use base qw(Slim::Player::Protocols::SqueezePlayDirect); - -use JSON::XS::VersionOneAndTwo; -use MIME::Base64 qw(decode_base64); -use Scalar::Util qw(blessed); -use URI::Escape qw(uri_escape); - -use Slim::Networking::SqueezeNetwork; -use Slim::Utils::Cache; -use Slim::Utils::Misc; -use Slim::Utils::Prefs; - -my $log = Slim::Utils::Log->addLogCategory( { - 'category' => 'plugin.spotifylogi', - 'defaultLevel' => 'ERROR', - 'description' => 'PLUGIN_SPOTIFYLOGI_MODULE_NAME', -} ); - -sub init { - # Make sure all player objects have authentication data stored. - # Whenever a player connects, do a quick lookup to get the playback - # authentication details. - Slim::Control::Request::subscribe( - sub { - __PACKAGE__->initAuthorization($_[0]->client); - }, - [['client'],['new','reconnect']] - ); -} - -sub initAuthorization { - my ($class, $client) = @_; - - if ( $client->model =~ /(fab4|baby)/ && !$client->pluginData('auth') ) { - $client->pluginData( auth => {} ); - - _getTrackAndAuth('spotify:track:4uLU6hMCjMI75M1A2tKUQC', { - client => $client, - }); - - return 1; - } -} - -sub canSeek { 1 } - -# Source for AudioScrobbler -sub audioScrobblerSource { - # P = Chosen by the user - return 'P'; -} - -# Suppress some messages during initial connection, at Spotify's request -sub suppressPlayersMessage { - my ( $class, $client, $song, $string ) = @_; - - if ( $string eq 'GETTING_STREAM_INFO' || $string eq 'CONNECTING_FOR' || $string eq 'BUFFERING' ) { - return 1; - } - - return; -} - -sub getNextTrack { - my ( $class, $song, $successCb, $errorCb ) = @_; - - my ($trackId) = $song->track()->url =~ m{spotify:/*(.+)}; - - _getTrackAndAuth($trackId, { - client => $song->master(), - song => $song, - cb => $successCb, - ecb => $errorCb, - }); -} - -sub _getTrackAndAuth { - my ($trackId, $params) = @_; - - Slim::Networking::SqueezeNetwork->new( - sub { - my $http = shift; - my $info = eval { from_json( $http->content ) }; - if ( $@ || $info->{error} ) { - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( 'getPlaybackInfo failed: ' . ( $@ || $info->{error} ) ); - } - - if ( my $errorCb = $http->params('ecb') ) { - $errorCb->( $@ || $info->{error} ); - } - } - else { - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( 'getPlaybackInfo ok: ' . Data::Dump::dump($info) ); - } - - if ( $info->{auth} && (my $client = $http->params('client')) ) { - $client->pluginData( auth => delete $info->{auth} ); - } - - if ( my $song = $http->params('song') ) { - $song->pluginData( info => $info ); - } - - if ( my $successCb = $http->params('cb') ) { - $successCb->(); - } - } - }, - sub { - my $http = shift; - - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( 'getPlaybackInfo failed: ' . $http->error ); - } - - if ( my $errorCb = $http->params('ecb') ) { - $errorCb->( $http->error ); - } - }, - $params - )->get( - Slim::Networking::SqueezeNetwork->url( - '/api/spotify/v1/playback/getPlaybackInfo?trackId=' . uri_escape($trackId), - ) - ); - - main::DEBUGLOG && $log->is_debug && $log->debug('Getting playback info from SN for ' . $trackId); -} - -sub explodePlaylist { - my ( $class, $client, $uri, $cb ) = @_; - - my $tracks = []; - - if ( $uri !~ /^spotify:track:/ ) { - Slim::Networking::SqueezeNetwork->new( - sub { - my $http = shift; - my $tracks = eval { from_json( $http->content ) }; - $cb->($tracks || []); - }, - sub { - $cb->([$uri]) - }, - { - client => $client - } - )->get( - Slim::Networking::SqueezeNetwork->url( - '/api/spotify/v1/playback/getTracksFromURI?uri=' . uri_escape($uri), - ) - ); - } - else { - $cb->([$uri]) - } -} - -sub canDirectStream { - my ( $class, $client, $url ) = @_; - - my ($handler) = $url =~ m{^spotify:/*(.+?)}; - - if ($handler && $client->can('spDirectHandlers') && $client->spDirectHandlers =~ /spotify/) { - # Rewrite URL if it came from Triode's plugin - $url =~ s{^spotify:track}{spotify://track}; - - return $url; - } -} - -sub onStream { - my ( $class, $client, $song ) = @_; - - # send spds packet with auth info - my $info = $song->pluginData('info'); - my $auth = $client->pluginData('auth'); - my $prefs = $info->{prefs}; - - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( - $client->id . ' Sending playback information (username ' . $auth->{username} . ', bitrate pref ' . $prefs->{bitrate} . ')' - ); - } - - my $data = pack( - 'cC/a*C/a*C/a*c', - 1, - $auth->{username}, - decode_base64( $auth->{password} ), - decode_base64( $auth->{iv} ), - $prefs->{bitrate} == 320 ? 1 : 0, - ); - - $client->sendFrame( spds => \$data ); -} - -sub getMetadataFor { - my ( $class, $client, $url, undef, $song ) = @_; - - my $icon = Slim::Networking::SqueezeNetwork->url('/static/images/icons/spotify/album.png'); - $song ||= $client->currentSongForUrl($url); - - # Rewrite URL if it came from Triode's plugin - $url =~ s{^spotify:track}{spotify://track}; - - if ( $song ||= $client->currentSongForUrl($url) ) { - if ( my $info = $song->pluginData('info') ) { - return { - artist => $info->{artist}, - album => $info->{album}, - title => $info->{title}, - duration => $info->{duration}, - cover => $info->{cover}, - icon => $icon, - bitrate => $info->{prefs}->{bitrate} . 'k VBR', - info_link => 'plugins/spotifylogi/trackinfo.html', - type => 'Ogg Vorbis (Spotify)', - } if $info->{title} && $info->{duration}; - } - } - - # Try to pull metadata from cache - my $cache = Slim::Utils::Cache->new; - - # If metadata is not here, fetch it so the next poll will include the data - my ($trackURI) = $url =~ m{spotify:/*(.+)}; - my $meta = $cache->get( 'spotify_meta_' . $trackURI ); - - if ( !$meta && !$client->master->pluginData('fetchingMeta') ) { - # Go fetch metadata for all tracks on the playlist without metadata - my @need; - - for my $track ( @{ Slim::Player::Playlist::playList($client) } ) { - my $trackURL = blessed($track) ? $track->url : $track; - if ( $trackURL =~ m{spotify:/*(.+)} ) { - my $id = $1; - if ( !$cache->get("spotify_meta_$id") ) { - push @need, $id; - } - } - } - - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( "Need to fetch metadata for: " . join( ', ', @need ) ); - } - - $client->master->pluginData( fetchingMeta => 1 ); - - my $metaUrl = Slim::Networking::SqueezeNetwork->url( - "/api/spotify/v1/playback/getBulkMetadata" - ); - - my $http = Slim::Networking::SqueezeNetwork->new( - \&_gotBulkMetadata, - \&_gotBulkMetadataError, - { - client => $client, - timeout => 60, - }, - ); - - $http->post( - $metaUrl, - 'Content-Type' => 'application/x-www-form-urlencoded', - 'trackIds=' . join( ',', @need ), - ); - } - - #$log->debug( "Returning metadata for: $url" . ($meta ? '' : ': default') ); - - if ( $song ) { - if ( $meta->{duration} && !($song->duration && $song->duration > 0) ) { - $song->duration($meta->{duration}); - } - - $song->pluginData( info => $meta ); - } - - return $meta || { - bitrate => '320k VBR', - type => 'Ogg Vorbis (Spotify)', - icon => $icon, - cover => $icon, - info_link => 'plugins/spotifylogi/trackinfo.html', - }; -} - -sub _gotBulkMetadata { - my $http = shift; - my $client = $http->params->{client}; - - $client->master->pluginData( fetchingMeta => 0 ); - - my $info = eval { from_json( $http->content ) }; - - if ( $@ || ref $info ne 'ARRAY' ) { - $log->error( "Error fetching track metadata: " . ( $@ || 'Invalid JSON response' ) ); - return; - } - - if ( main::DEBUGLOG && $log->is_debug ) { - $log->debug( "Caching metadata for " . scalar( @{$info} ) . " tracks" ); - } - - # Cache metadata - my $cache = Slim::Utils::Cache->new; - my $icon = Slim::Networking::SqueezeNetwork->url('/static/images/icons/spotify/album.png'); - - for my $track ( @{$info} ) { - next unless ref $track eq 'HASH'; - - # cache the metadata we need for display - my $trackId = delete $track->{trackId}; - - my $meta = { - %{$track}, - bitrate => '320k VBR', - type => 'Ogg Vorbis (Spotify)', - info_link => 'plugins/spotifylogi/trackinfo.html', - icon => $icon, - }; - - $cache->set( 'spotify_meta_' . $trackId, $meta, 86400 ); - } - - # Update the playlist time so the web will refresh, etc - $client->currentPlaylistUpdateTime( Time::HiRes::time() ); - - Slim::Control::Request::notifyFromArray( $client, [ 'newmetadata' ] ); -} - -sub _gotBulkMetadataError { - my $http = shift; - my $client = $http->params('client'); - my $error = $http->error; - - $client->master->pluginData( fetchingMeta => 0 ); - - $log->warn("Error getting track metadata from SN: $error"); -} - -# URL used for CLI trackinfo queries -sub trackInfoURL { - my ( $class, $client, $url ) = @_; - - my ($trackId) = $url =~ m{spotify:/*(.+)}; - - # SN URL to fetch track info menu - my $trackInfoURL = Slim::Networking::SqueezeNetwork->url( - '/api/spotify/v1/opml/track?uri=spotify:' . $trackId - ); - - return $trackInfoURL; -} - -sub getIcon { - my ( $class, $url ) = @_; - - return Slim::Plugin::SpotifyLogi::Plugin->_pluginDataFor('icon'); -} - -1; diff --git a/Slim/Plugin/SpotifyLogi/install.xml b/Slim/Plugin/SpotifyLogi/install.xml deleted file mode 100644 index 3675c444bd6..00000000000 --- a/Slim/Plugin/SpotifyLogi/install.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - 214FD47A-4857-4E1F-9651-521B5F2D8F70 - PLUGIN_SPOTIFYLOGI_MODULE_NAME - Slim::Plugin::SpotifyLogi::Plugin - 1.0 - PLUGIN_SPOTIFYLOGI_MODULE_NAME_DESC - Logitech - enabled - true - http://www.mysqueezebox.com/appgallery/Spotify - plugins/SpotifyLogi/html/images/icon.png - 2 - - Logitech Media Server - 7.5 - * - - diff --git a/Slim/Plugin/SpotifyLogi/strings.txt b/Slim/Plugin/SpotifyLogi/strings.txt deleted file mode 100644 index 251003349c7..00000000000 --- a/Slim/Plugin/SpotifyLogi/strings.txt +++ /dev/null @@ -1,319 +0,0 @@ -PLUGIN_SPOTIFYLOGI_MODULE_NAME - CS Služba Spotify - DA Spotify - DE Spotify - EN Spotify - ES Spotify - FI Spotify - FR Spotify - IT Spotify - NL Spotify - NO Spotify - PL Spotify - RU Spotify - SV Spotify - -PLUGIN_SPOTIFYLOGI_MODULE_NAME_DESC - CS Oficiální zásuvný modul služby Spotify. Poslouchejte službu Spotify na Radio nebo Touch. - DA Officielt Spotify-udvidelsesmodul. Lyt til Spotify på Radio eller Touch. - DE Offizielles Spotify-Plugin. Nutzen Sie Spotify über Ihr Squeezebox Radio oder Ihre Squeezebox Touch. - EN Official Spotify plugin. Listen to Spotify on your Radio or Touch. - ES Complemento de Spotify oficial. Escuche Spotify en su dispositivo Radio o Touch. - FI Virallinen Spotify-laajennus. Kuuntele musiikkia Spotifysta radion tai Touchin välityksellä. - FR Plugin Spotify officiel. Ecoutez Spotify sur votre Squeezebox Radio ou votre Squeezebox Touch. - IT Plugin ufficiale di Spotify. Consente di ascoltare Spotify su Radio o Touch. - NL Officiële plug-in van Spotify. Luister naar Spotify op je Radio of Touch. - NO Offisiell plugin-modul for Spotify. Lytt til Spotify på en Radio- eller Touch-spiller. - PL Oficjalny dodatek usługi Spotify. Umożliwia słuchanie usługi Spotify w urządzeniu Radio lub Touch. - RU Официальный подключаемый модуль Spotify. Слушать Spotify на плеере Squeezebox Radio или Squeezebox Touch. - SV Officiellt plugin-program för Spotify. Lyssna på Spotify via din radio eller Touch. - -PLUGIN_SPOTIFYLOGI_ERROR - CS Chyba služby Spotify - DA Spotify-fejl - DE Spotify-Fehler - EN Spotify Error - ES Error de Spotify - FI Spotify-virhe - FR Erreur de Spotify - IT Errore Spotify - NL Fout in Spotify - NO Spotify-feil - PL Błąd Spotify - RU Ошибка Spotify - SV Spotify-fel - -PLUGIN_SPOTIFYLOGI_ON_SPOTIFY - CS Na službě Spotify - DA På Spotify - DE In Spotify - EN On Spotify - ES En Spotify - FI Spotifyssä - FR Sur Spotify - IT Su Spotify - NL Over Spotify - NO På Spotify - PL W Spotify - RU На Spotify - SV På Spotify - -PLUGIN_SPOTIFYLOGI_RECOMMENDATIONS - DE Spotify Empfehlungen (basierend auf aktueller Wiedergabeliste) - EN Spotify Recommendations (based on the current playlist) - -SPOTIFY_ERROR_3 - CS Tuto stopu nelze přehrát. - DA Dette nummer kan ikke afspilles. - DE Dieser Titel kann nicht wiedergegeben werden. - EN This track is not playable. - ES Esta pista no se puede reproducir. - FI Tätä kappaletta ei voida toistaa. - FR Ce morceau n'est pas lisible. - IT Questo brano non è riproducibile. - NL Dit nummer kan niet worden afgespeeld. - NO Dette sporet kan ikke spilles av. - PL Tego utworu nie można odtworzyć. - RU Воспроизведение этой дорожки невозможно. - SV Det går inte att spela det här spåret. - -SPOTIFY_ERROR_4 - CS Zdroj nebyl načten. - DA Resursen kan ikke indlæses. - DE Ressource nicht geladen. - EN Resource not loaded. - ES Recurso no cargado. - FI Resurssia ei ladattu. - FR Ressource non chargée. - IT Risorsa non caricata. - NL Hulpbron niet geladen. - NO Ressursen ble ikke lastet inn. - PL Zasób nie został załadowany. - RU Ресурс не загружен. - SV Resursen är inte laddad. - -SPOTIFY_ERROR_5 - CS Neplatný klíč aplikace. - DA Ugyldig programnøgle. - DE Ungültiger Anwendungsschlüssel - EN Invalid application key. - ES Clave de aplicación no válida. - FI Virheellinen sovellutusavain. - FR Clé d'application non valide. - IT Tasto dedicato non valido. - NL Ongeldige toepassingssleutel. - NO Ugyldig programnøkkel. - PL Niepoprawny klucz aplikacji. - RU Недопустимый ключ приложения. - SV Ogiltig programnyckel. - -SPOTIFY_ERROR_6 - CS Neplatné uživatelské jméno nebo heslo. - DA Ugyldigt brugernavn eller adgangskode. - DE Benutzername oder Kennwort ungültig. - EN Invalid username or password. - ES Contraseña o nombre de usuario no válido. - FI Virheellinen käyttäjätunnus tai salasana. - FR Nom d'utilisateur ou mot de passe non valide. - IT Nome utente o password non validi. - NL Gebruikersnaam of wachtwoord ongeldig. - NO Ugyldig brukernavn eller passord. - PL Nieprawidłowa nazwa użytkownika lub hasło. - RU Недействительные имя пользователя или пароль. - SV Ogiltigt användarnamn eller lösenord. - -SPOTIFY_ERROR_7 - CS Uživatel byl zakázán. - DA Brugeren er blevet bortvist. - DE Der Benutzer wurde gesperrt. - EN User has been banned. - ES El usuario ha sido vetado. - FI Käyttäjä on estetty. - FR L'utilisateur a été banni. - IT L'utente è stato escluso. - NL Gebruiker is verbannen. - NO Brukeren er sperret. - PL Użytkownik został zablokowany. - RU Пользователь заблокирован. - SV Användaren är avstängd. - -SPOTIFY_ERROR_8 - CS Nelze se spojit se serverem služby Spotify. - DA Det var ikke muligt at forbinde til Spotify-serveren. - DE Es kann keine Verbindung zum Spotify-Server hergestellt werden. - EN Unable to contact Spotify server. - ES No se puede establecer contacto con servidor de Spotify. - FI Spotify-palvelimeen ei saada yhteyttä. - FR Impossible de contacter le serveur Spotify. - IT Impossibile contattare il server Spotify. - NL Kan geen verbinding maken met Spotify-server. - NO Kan ikke kontakte Spotify-serveren. - PL Nie można skontaktować się z serwerem Spotify. - RU Не удается связаться с сервером Spotify. - SV Det går inte att få kontakt med Spotify-servern. - -SPOTIFY_ERROR_9 - CS Klient příliš starý, aktualizujte firmware. - DA Klientprogrammet er forældet. Du skal opdatere firmwaren. - DE Client-Version zu alt. Aktualisieren Sie Ihre Firmware. - EN Client too old, please update your firmware. - ES Cliente demasiado antiguo, debe actualizar el firmware. - FI Asiakas on liian vanha, päivitä laiteohjelmisto. - FR Le client est obsolète, veuillez mettre le micrologiciel à jour. - IT Client obsoleto. Aggiornare il firmware. - NL Client is te oud. Update je firmware. - NO Klienten er utdatert, oppdater fastvaren. - PL Zbyt stara wersja klienta. Zaktualizuj oprogramowanie układowe. - RU Версия клиента устарела, обновите микропрограмму. - SV Klienten är för gammal. Uppdatera programvaran. - -SPOTIFY_ERROR_10 - CS Vyskytla se neznámá chyba. - DA Der opstod en ukendt fejl. - DE Ein unbekannter Fehler ist aufgetreten. - EN An unknown error occurred. - ES Se ha producido un error desconocido. - FI Tapahtui tuntematon virhe. - FR Une erreur inconnue s'est produite. - IT Errore sconosciuto. - NL Er is een onbekende fout opgetreden. - NO Det oppsto en ukjent feil. - PL Wystąpił nieznany błąd. - RU Произошла неизвестная ошибка. - SV Ett okänt fel uppstod. - -SPOTIFY_ERROR_15 - CS Pro používání služby Spotify je třeba mít zřízen účet premium. - DA Du skal have et betalingsabonnement for at kunne bruge Spotify. - DE Zur Verwendung von Spotify ist ein Premium-Abonnement erforderlich. - EN A premium account is required to use Spotify. - ES Para usar Spotify se necesita una cuenta Premium. - FI Spotifyn käyttöön vaaditaan maksullinen tili. - FR Un compte payant est requis pour utiliser Spotify. - IT Per utilizzare Spotify è necessario disporre di un account Premium. - NL Je moet een Premium-account hebben om Spotify te kunnen gebruiken. - NO Du trenger en betalingskonto for å kunne bruke Spotify. - PL Do korzystania z usługi Spotify potrzebne jest konto premium. - RU Для использования Spotify требуется учетная запись Premium. - SV Det krävs ett premiumkonto för att använda Spotify. - -SPOTIFY_ERROR_16 - CS Vyskytla se neznámá chyba, zkuste to znovu. - DA Der opstod en ukendt fejl. Prøv igen. - DE Ein unbekannter Fehler ist aufgetreten. Versuchen Sie es erneut. - EN An unknown error occurred, please try again. - ES Se ha producido un error desconocido. Vuelva a intentarlo. - FI Tapahtui tuntematon virhe, yritä uudelleen. - FR Une erreur inconnue s'est produite. Veuillez réessayer. - IT Errore sconosciuto. Riprovare. - NL Er is een onbekende fout opgetreden. Probeer het opnieuw. - NO Det oppsto en ukjent feil. Prøv igjen. - PL Wystąpił nieznany błąd. Spróbuj ponownie. - RU Произошла неизвестная ошибка, повторите попытку. - SV Ett okänt fel uppstod. Försök igen. - -SPOTIFY_ERROR_17 - CS Zdroj se právě načítá, zkuste to znovu. - DA Resursen er ved at blive indlæst. Prøv igen. - DE Ressource wird geladen. Versuchen Sie es erneut. - EN The resource is currently loading, please try again. - ES El recurso se está cargando; vuelva a intentarlo. - FI Resurssia ladataan parhaillaan, kokeile myöhemmin uudelleen. - FR La ressource est en cours de chargement, veuillez réessayer. - IT La risorsa è in fase di caricamento. Riprovare. - NL De hulpbron wordt momenteel geladen. Probeer het opnieuw. - NO Ressursen lastes inn, prøv på nytt. - PL Zasób jest aktualnie ładowany, spróbuj ponownie. - RU Ресурс загружается, повторите попытку. - SV Resursen håller på att laddas. Försök igen om en stund. - -SPOTIFY_ERROR_18 - CS Nelze najít proud pro tuto stopu. - DA Det var ikke muligt at finde en stream med dette nummer. - DE Für diesen Titel wurde kein Stream gefunden. - EN Unable to find a stream for this track. - ES No se ha encontrado una secuencia para esta pista. - FI Tälle kappaleelle ei löydy virtaa. - FR Impossible de trouver un flux pour ce morceau. - IT Impossibile trovare lo stream di questo brano. - NL Kan geen stream vinden voor dit nummer. - NO Finner ingen strøm for dette sporet. - PL Nie można znaleźć strumienia tego utworu. - RU Не удается найти поток для этой дорожки. - SV Det går inte att hitta någon dataström för det här spåret. - -SPOTIFY_ERROR_19 - CS Požadovaná operace není povolena. - DA Det du forsøger, er ikke tilladt. - DE Angeforderter Vorgang ist nicht zulässig. - EN Requested operation is not allowed. - ES No se permite la operación solicitada. - FI Pyydetty toimenpide ei ole sallittu. - FR L'opération demandée n'est pas autorisée. - IT Operazione richiesta non consentita. - NL Gevraagde bewerking is niet toegestaan. - NO Forespurt handling er ikke tillatt. - PL Żądana operacja jest niedozwolona. - RU Запрошенная операция запрещена. - SV Den begärda åtgärden är inte tillåten. - -SPOTIFY_ERROR_100 - CS Nebyla zadána adresa URI služby Spotify pro přehrávání. - DA Der er ikke angivet en URI-adresse for Spotify der kan afspilles. - DE Kein Spotify-URI für die Wiedergabe angegeben - EN No Spotify URI specified for playback. - ES No se ha especificado URI de Spotify para reproducción. - FI Ei määrittyä Spotifyn URI-osoitetta toistolle - FR Aucun URI Spotify spécifié pour la lecture. - IT Nessun URI Spotify specificato per la riproduzione. - NL Geen Spotify-URI opgegeven voor afspelen. - NO Ingen Spotify-URI angitt for avspilling. - PL Nie określono adresu URI Spotify do odtwarzania. - RU Не указан URI Spotify для воспроизведения. - SV Ingen Spotify-URI har angetts för uppspelning. - -SPOTIFY_ERROR_101 - CS Neplatná adresa URI stopy služby Spotify. - DA Ugyldig URI-adresse til nummer på Spotify. - DE Ungültiger Spotify-Titel-URI. - EN Invalid Spotify track URI. - ES URI de pista de Spotify no válido. - FI Virheellinen Spotify-kappaleen URI-osoite. - FR URI de morceau Spotify non valide. - IT URI brano Spotify non valido. - NL Ongeldige URI van Spotify-nummer. - NO Ugyldig Spotify-URI for sporet. - PL Nieprawidłowy adres URI utworu z usługi Spotify. - RU Недопустимый URI дорожки Spotify. - SV Ogiltig URI för spår i Spotify. - -SPOTIFY_ERROR_102 - CS Služba Spotify byla zastavena - váš účet je již používán jinde. - DA Spotify er stoppet fordi din konto er i brug et andet sted. - DE Spotify wurde angehalten, weil Ihr Konto anderweitig verwendet wird. - EN Spotify has been stopped because your account is in use somewhere else. - ES Spotify se ha detenido porque su cuenta ya está en uso en otra ubicación. - FI Spotify on pysäytetty, koska tilisi on käytössä jossain muualla. - FR Spotify a été arrêté car votre compte est en cours d'utilisation sur un autre ordinateur ou dispositif. - IT Spotify è stato arrestato poiché l'account è utilizzato da un'altra applicazione. - NL Spotify is gestopt, omdat je account ergens anders wordt gebruikt. - NO Spotify er stoppet, fordi kontoen din er i bruk et annet sted. - PL Nastąpiło zatrzymanie usługi Spotify, ponieważ Twoje konto jest używane gdzie indziej. - RU Работа Spotify остановлена, так как ваша учетная запись используется на другом компьютере. - SV Spotify har stoppats eftersom ditt konto redan används någon annanstans. - -SPOTIFY_ERROR_103 - CS Tato stopa není dostupná ve vašem regionu. - DA Du kan ikke lytte til dette nummer i dit område. - DE Dieser Titel ist in Ihrer Region nicht verfügbar. - EN This track is not available in your region. - ES Esta pista no está disponible en su región. - FI Tämä kappale ei ole saatavilla alueellasi. - FR Ce morceau n'est pas disponible dans votre région. - IT Questo brano non è disponibile nel paese di residenza. - NL Dit nummer is niet verkrijgbaar in jouw regio. - NO Dette sporet er ikke tilgjengelig i din region. - PL Ten utwór jest niedostępny w Twoim regionie. - RU Эта дорожка недоступна в вашем регионе. - SV Det här spåret är inte tillgängligt i din region. - diff --git a/Slim/Plugin/TT/Clients.pm b/Slim/Plugin/TT/Clients.pm index 7b085f85785..7dd25959006 100644 --- a/Slim/Plugin/TT/Clients.pm +++ b/Slim/Plugin/TT/Clients.pm @@ -1,7 +1,6 @@ package Slim::Plugin::TT::Clients; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/TT/Prefs.pm b/Slim/Plugin/TT/Prefs.pm index 64bf5d47fae..63382760351 100644 --- a/Slim/Plugin/TT/Prefs.pm +++ b/Slim/Plugin/TT/Prefs.pm @@ -1,7 +1,6 @@ package Slim::Plugin::TT::Prefs; -# $Id: Prefs.pm 1757 2005-01-18 21:22:50Z dsully $ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/UPnP/Discovery.pm b/Slim/Plugin/UPnP/Discovery.pm index a28ba3d8f80..3d322a72d8f 100644 --- a/Slim/Plugin/UPnP/Discovery.pm +++ b/Slim/Plugin/UPnP/Discovery.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::Discovery; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/Discovery.pm 78831 2011-07-25T16:48:09.710754Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. # This module handles UPnP 1.0 discovery advertisements and responses to search requests # Reference: http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf @@ -12,7 +15,7 @@ use strict; use Digest::MD5 qw(md5_hex); use HTTP::Date; -use Network::IPv4Addr (); +use Net::IPv4Addr (); use Socket; use Slim::Networking::Select; @@ -76,7 +79,7 @@ sub init { for (my $i = 0; $i < @$addresses; $i++) { if (my ($addr) = $addresses->[$i] =~ /^(\d+\.\d+.\d+.\d+)$/ ){ push @addresses, $addr; - $CIDR{$addr} = Network::IPv4Addr::ipv4_parse($addr, $subnets->[$i]); + $CIDR{$addr} = Net::IPv4Addr::ipv4_parse($addr, $subnets->[$i]); } } } @@ -90,7 +93,7 @@ sub init { next unless $if->is_running && $if->is_multicast; my $addr = $if->address || next; push @addresses, $addr; - $CIDR{$addr} = Network::IPv4Addr::ipv4_parse($addr, $if->netmask); + $CIDR{$addr} = Net::IPv4Addr::ipv4_parse($addr, $if->netmask); } }; } @@ -479,7 +482,7 @@ sub _advertise { # Determine which of our local addresses is on the same subnet as the destination my $local_addr; for my $a ( keys %SOCKS ) { - if ( exists $CIDR{$a} && Network::IPv4Addr::ipv4_in_network($CIDR{$a}, $dest->{addr}) ) { + if ( exists $CIDR{$a} && Net::IPv4Addr::ipv4_in_network($CIDR{$a}, $dest->{addr}) ) { $local_addr = $a; last; } diff --git a/Slim/Plugin/UPnP/Events.pm b/Slim/Plugin/UPnP/Events.pm index 114598827d7..44c7c53336b 100644 --- a/Slim/Plugin/UPnP/Events.pm +++ b/Slim/Plugin/UPnP/Events.pm @@ -1,7 +1,10 @@ package Slim::Plugin::UPnP::Events; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/Events.pm 75368 2010-12-16T04:09:11.731914Z andy $ -# +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + # Eventing functions use strict; diff --git a/Slim/Plugin/UPnP/MediaRenderer.pm b/Slim/Plugin/UPnP/MediaRenderer.pm index fe36c63f7a0..2a234664534 100644 --- a/Slim/Plugin/UPnP/MediaRenderer.pm +++ b/Slim/Plugin/UPnP/MediaRenderer.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaRenderer; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaRenderer.pm 75368 2010-12-16T04:09:11.731914Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/MediaRenderer/AVTransport.pm b/Slim/Plugin/UPnP/MediaRenderer/AVTransport.pm index f6440cf0b06..34b9ef6def1 100644 --- a/Slim/Plugin/UPnP/MediaRenderer/AVTransport.pm +++ b/Slim/Plugin/UPnP/MediaRenderer/AVTransport.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaRenderer::AVTransport; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaRenderer/AVTransport.pm 75368 2010-12-16T04:09:11.731914Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/MediaRenderer/ConnectionManager.pm b/Slim/Plugin/UPnP/MediaRenderer/ConnectionManager.pm index 0823ba26aec..c996ec15b30 100644 --- a/Slim/Plugin/UPnP/MediaRenderer/ConnectionManager.pm +++ b/Slim/Plugin/UPnP/MediaRenderer/ConnectionManager.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaRenderer::ConnectionManager; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaRenderer/ConnectionManager.pm 75368 2010-12-16T04:09:11.731914Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/MediaRenderer/RenderingControl.pm b/Slim/Plugin/UPnP/MediaRenderer/RenderingControl.pm index 5282cc9dc08..75b3ded0fe3 100644 --- a/Slim/Plugin/UPnP/MediaRenderer/RenderingControl.pm +++ b/Slim/Plugin/UPnP/MediaRenderer/RenderingControl.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaRenderer::RenderingControl; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaRenderer/RenderingControl.pm 75368 2010-12-16T04:09:11.731914Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/MediaServer.pm b/Slim/Plugin/UPnP/MediaServer.pm index b168ab5feef..36b9e174b1d 100644 --- a/Slim/Plugin/UPnP/MediaServer.pm +++ b/Slim/Plugin/UPnP/MediaServer.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaServer; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaServer.pm 78831 2011-07-25T16:48:09.710754Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/MediaServer/ConnectionManager.pm b/Slim/Plugin/UPnP/MediaServer/ConnectionManager.pm index 13ecc7aa58b..a687878a664 100644 --- a/Slim/Plugin/UPnP/MediaServer/ConnectionManager.pm +++ b/Slim/Plugin/UPnP/MediaServer/ConnectionManager.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaServer::ConnectionManager; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaServer/ConnectionManager.pm 75368 2010-12-16T04:09:11.731914Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/MediaServer/ContentDirectory.pm b/Slim/Plugin/UPnP/MediaServer/ContentDirectory.pm index 4ce1a88e6fa..9d514395dd6 100644 --- a/Slim/Plugin/UPnP/MediaServer/ContentDirectory.pm +++ b/Slim/Plugin/UPnP/MediaServer/ContentDirectory.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaServer::ContentDirectory; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaServer/ContentDirectory.pm 78831 2011-07-25T16:48:09.710754Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/MediaServer/MediaReceiverRegistrar.pm b/Slim/Plugin/UPnP/MediaServer/MediaReceiverRegistrar.pm index 8e375dac7fc..8ee02b84dcb 100644 --- a/Slim/Plugin/UPnP/MediaServer/MediaReceiverRegistrar.pm +++ b/Slim/Plugin/UPnP/MediaServer/MediaReceiverRegistrar.pm @@ -1,6 +1,9 @@ package Slim::Plugin::UPnP::MediaServer::MediaReceiverRegistrar; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/MediaServer/MediaReceiverRegistrar.pm 75368 2010-12-16T04:09:11.731914Z andy $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; diff --git a/Slim/Plugin/UPnP/Plugin.pm b/Slim/Plugin/UPnP/Plugin.pm index c44692c66c3..5bd511ec5bc 100644 --- a/Slim/Plugin/UPnP/Plugin.pm +++ b/Slim/Plugin/UPnP/Plugin.pm @@ -1,7 +1,10 @@ package Slim::Plugin::UPnP::Plugin; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/Plugin.pm 78831 2011-07-25T16:48:09.710754Z andy $ -# +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + # UPnP/DLNA Media Interface # Andy Grundman # andy@slimdevices.com diff --git a/Slim/Plugin/UPnP/SOAPServer.pm b/Slim/Plugin/UPnP/SOAPServer.pm index 7ac6040056b..1a5a6a0fa73 100644 --- a/Slim/Plugin/UPnP/SOAPServer.pm +++ b/Slim/Plugin/UPnP/SOAPServer.pm @@ -1,7 +1,10 @@ package Slim::Plugin::UPnP::SOAPServer; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Plugin/UPnP/SOAPServer.pm 76276 2011-02-01T19:44:19.488696Z andy $ -# +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + # SOAP handling functions. # Note that SOAP::Lite is only used for parsing requests and generating responses, # it does not send or receive directly from the network. @@ -133,7 +136,7 @@ sub processControl { $body = undef; } - main::INFOLOG && $log->is_info && $log->info( "Invoking ${serviceClass}->${method}( " . Data::Dump::dump($body) . ' )' ); + main::DEBUGLOG && $log->is_debug && $log->debug( "Invoking ${serviceClass}->${method}( " . Data::Dump::dump($body) . ' )' ); # Invoke the method my @result = eval { $serviceClass->$method( $client, $body || {}, $request->headers, $request->header('Host') || $request->uri->host ) }; diff --git a/Slim/Plugin/UPnP/install.xml b/Slim/Plugin/UPnP/install.xml index 4431f6bf894..1d4e60e4cbd 100644 --- a/Slim/Plugin/UPnP/install.xml +++ b/Slim/Plugin/UPnP/install.xml @@ -7,7 +7,7 @@ PLUGIN_UPNP_MODULE_NAME_DESC Andy Grundman andy@slimdevices.com - enabled + disabled http://www.slimdevices.com/ 2 diff --git a/Slim/Plugin/Visualizer/Plugin.pm b/Slim/Plugin/Visualizer/Plugin.pm index 0e3d51227de..265bd97b060 100644 --- a/Slim/Plugin/Visualizer/Plugin.pm +++ b/Slim/Plugin/Visualizer/Plugin.pm @@ -1,8 +1,6 @@ package Slim::Plugin::Visualizer::Plugin; -# $Id: $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/Visualizer/strings.txt b/Slim/Plugin/Visualizer/strings.txt index 602ee051a22..76c0bcca461 100644 --- a/Slim/Plugin/Visualizer/strings.txt +++ b/Slim/Plugin/Visualizer/strings.txt @@ -1,7 +1,5 @@ # String file for Visualizer plugin -# $Id: strings.txt 10821 2006-12-01 21:44:53Z adrian $ - PLUGIN_SCREENSAVER_VISUALIZER CS Vizualizační spořič obrazovky DA Visualiseringspauseskærm diff --git a/Slim/Plugin/WiMP/Plugin.pm b/Slim/Plugin/WiMP/Plugin.pm index b8c750888f6..f82bf80e566 100644 --- a/Slim/Plugin/WiMP/Plugin.pm +++ b/Slim/Plugin/WiMP/Plugin.pm @@ -20,7 +20,7 @@ sub initPlugin { ); Slim::Player::ProtocolHandlers->registerIconHandler( - qr/squeezenetwork\.com.*\/wimp\//, + qr/mysqueezebox\.com.*\/wimp\//, sub { return $class->_pluginDataFor('icon'); } ); diff --git a/Slim/Plugin/WiMP/ProtocolHandler.pm b/Slim/Plugin/WiMP/ProtocolHandler.pm index dc8983aa2d4..2ff211302f6 100644 --- a/Slim/Plugin/WiMP/ProtocolHandler.pm +++ b/Slim/Plugin/WiMP/ProtocolHandler.pm @@ -1,6 +1,9 @@ package Slim::Plugin::WiMP::ProtocolHandler; -# $Id: ProtocolHandler.pm 30836 2010-05-28 20:13:33Z agrundman $ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. use strict; use base qw(Slim::Player::Protocols::HTTP); @@ -26,19 +29,27 @@ sub isRemote { 1 } sub getFormatForURL { my ($class, $url) = @_; - + my ($trackId, $format) = _getStreamParams( $url ); return $format; } # default buffer 3 seconds of 256kbps MP3/768kbps FLAC audio +my %bufferSecs = ( + flac => 80, + flc => 80, + mp3 => 32, + mp4 => 40 +); + sub bufferThreshold { my ($class, $client, $url) = @_; - $url = $client->playingSong()->track()->url() unless $url =~ /\.(?:fla?c|mp3)$/; - - my ($trackId, $format) = _getStreamParams( $url ); - return ($format eq 'flac' ? 96 : 32) * ($prefs->get('bufferSecs') || 3); + $url = $client->playingSong()->track()->url() unless $url =~ /\.(fla?c|mp[34])/; + my $ext = $1; + + my ($trackId, $format) = _getStreamParams($url); + return ($bufferSecs{$format} || $bufferSecs{$ext} || 40) * ($prefs->get('bufferSecs') || 3); } sub canSeek { 1 } @@ -49,21 +60,29 @@ sub new { my $args = shift; my $client = $args->{client}; - + my $song = $args->{song}; my $streamUrl = $song->streamUrl() || return; my ($trackId, $format) = _getStreamParams( $args->{url} || '' ); - + main::DEBUGLOG && $log->debug( 'Remote streaming TIDAL track: ' . $streamUrl ); my $sock = $class->SUPER::new( { url => $streamUrl, song => $args->{song}, client => $client, - bitrate => $format eq 'flac' ? 800_000 : 256_000, + bitrate => _getBitrate($format), } ) || return; - - ${*$sock}{contentType} = $format eq 'flac' ? 'audio/flac' : 'audio/mpeg'; + + if ($format eq 'flac') { + ${*$sock}{contentType} = 'audio/flac'; + } + elsif ($format =~ /mp4|aac/) { + ${*$sock}{contentType} = 'audio/aac'; + } + else { + ${*$sock}{contentType} = 'audio/mpeg'; + } return $sock; } @@ -71,7 +90,7 @@ sub new { # Avoid scanning sub scanUrl { my ( $class, $url, $args ) = @_; - + $args->{cb}->( $args->{song}->currentTrack() ); } @@ -91,58 +110,58 @@ sub canSkip { 1 } sub handleDirectError { my ( $class, $client, $url, $response, $status_line ) = @_; - + main::DEBUGLOG && $log->debug("Direct stream failed: [$response] $status_line\n"); - + $client->controller()->playerStreamingFailed($client, 'PLUGIN_WIMP_STREAM_FAILED'); } sub _handleClientError { my ( $error, $client, $params ) = @_; - + my $song = $params->{song}; - + return if $song->pluginData('abandonSong'); - + # Tell other clients to give up $song->pluginData( abandonSong => 1 ); - + $params->{errorCb}->($error); } sub getNextTrack { my ( $class, $song, $successCb, $errorCb ) = @_; - + my $url = $song->track()->url; - + $song->pluginData( abandonSong => 0 ); - + my $params = { song => $song, url => $url, successCb => $successCb, errorCb => $errorCb, }; - + _getTrack($params); } sub _getTrack { my $params = shift; - + my $song = $params->{song}; my $client = $song->master(); - + return if $song->pluginData('abandonSong'); - + # Get track URL for the next track my ($trackId, $format) = _getStreamParams( $params->{url} ); - + if (!$trackId) { _gotTrackError( $client->string('PLUGIN_WIMP_INVALID_TRACK_ID'), $client, $params ); return; } - + my $http = Slim::Networking::SqueezeNetwork->new( sub { my $http = shift; @@ -151,53 +170,55 @@ sub _getTrack { if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( 'getTrack failed: ' . ( $@ || $info->{error} ) ); } - + _gotTrackError( $@ || $info->{error}, $client, $params ); } else { #if ( main::DEBUGLOG && $log->is_debug ) { # $log->debug( 'getTrack ok: ' . Data::Dump::dump($info) ); #} - + _gotTrack( $client, $info, $params ); } }, sub { my $http = shift; - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( 'getTrack failed: ' . $http->error ); } - + _gotTrackError( $http->error, $client, $params ); }, { client => $client, }, ); - + main::DEBUGLOG && $log->is_debug && $log->debug('Getting next track playback info from SN for ' . $params->{url}); - + $http->get( Slim::Networking::SqueezeNetwork->url( - '/api/wimp/v1/playback/getMediaURL?trackId=' . $trackId + sprintf('/api/wimp/v1/playback/getMediaURL?trackId=%s&format=%s', $trackId, $format) ) ); } sub _gotTrack { my ( $client, $info, $params ) = @_; - - my $song = $params->{song}; - - return if $song->pluginData('abandonSong'); - + + my $song = $params->{song}; + + return if $song->pluginData('abandonSong'); + # Save the media URL for use in strm $song->streamUrl($info->{url}); + my ($trackId, $format) = _getStreamParams( $params->{url} ); + # Save all the info $song->pluginData( info => $info ); - + # Cache the rest of the track's metadata my $icon = Slim::Plugin::WiMP::Plugin->_pluginDataFor('icon'); my $meta = { @@ -206,19 +227,19 @@ sub _gotTrack { title => $info->{title}, cover => $info->{cover} || $icon, duration => $info->{duration}, - bitrate => $params->{url} =~ /\.flac/ ? 'PCM VBR' : ($info->{bitrate} . 'k CBR'), - type => $params->{url} =~ /\.flac/ ? 'FLAC' : 'MP3', + bitrate => $format eq 'flac' ? 'PCM VBR' : ($info->{bitrate} . 'k CBR'), + type => lc($format) eq 'mp4' ? 'AAC' : uc($format), info_link => 'plugins/wimp/trackinfo.html', icon => $icon, }; - + $song->duration( $info->{duration} ); - + my $cache = Slim::Utils::Cache->new; $cache->set( 'wimp_meta_' . $info->{id}, $meta, 86400 ); $params->{successCb}->(); - + # trigger playback statistics update if ( $info->{duration} > 2) { # we're asked to report back if a track has been played halfway through @@ -231,7 +252,7 @@ sub _gotTrack { sub _gotTrackError { my ( $error, $client, $params ) = @_; - + main::DEBUGLOG && $log->debug("Error during getTrackInfo: $error"); return if $params->{song}->pluginData('abandonSong'); @@ -241,7 +262,7 @@ sub _gotTrackError { sub canDirectStreamSong { my ( $class, $client, $song ) = @_; - + # We need to check with the base class (HTTP) to see if we # are synced or if the user has set mp3StreamingMethod return $class->SUPER::canDirectStream( $client, $song->streamUrl(), $class->getFormatForURL($song->track->url()) ); @@ -250,25 +271,59 @@ sub canDirectStreamSong { # parseHeaders is used for proxied streaming sub parseHeaders { my ( $self, @headers ) = @_; - + __PACKAGE__->parseDirectHeaders( $self->client, $self->url, @headers ); - + return $self->SUPER::parseHeaders( @headers ); } sub parseDirectHeaders { my ( $class, $client, $url, @headers ) = @_; - # XXX - parse bitrate #main::DEBUGLOG && $log->is_debug && $log->debug(Data::Dump::dump(@headers)); - - my $isFlac = grep m{Content.*audio/(?:x-|)flac}i, @headers; - my $bitrate = $isFlac ? 800_000 : 256_000; + my ($length, $bitrate, $ct); + foreach my $header (@headers) { + if ( $header =~ /^Content-Length:\s*(.*)/i ) { + $length = $1; + } + elsif ( $header =~ /^Content-Type:\s*(\S*)/) { + $ct = Slim::Music::Info::mimeToType($1); + } + } + + $ct = 'aac' if !$ct || $ct eq 'mp4'; + + if ( $ct eq 'flc' && $length && (my $song = $client->streamingSong()) ) { + $bitrate = int($length/$song->duration*8); + + $url = $url->url if blessed $url; + my ($trackId) = _getStreamParams( $url ); + + if ($trackId) { + my $cache = Slim::Utils::Cache->new; + my $meta = $cache->get('wimp_meta_' . $trackId); + if ($meta && ref $meta) { + $meta->{bitrate} = sprintf("%.0f" . Slim::Utils::Strings::string('KBPS'), $bitrate/1000); + $cache->set( 'wimp_meta_' . $trackId, $meta, 86400 ); + } + } + } + + $bitrate ||= _getBitrate($ct); $client->streamingSong->bitrate($bitrate); # ($title, $bitrate, $metaint, $redir, $contentType, $length, $body) - return (undef, $bitrate, 0, '', $isFlac ? 'flc' : 'mp3'); + return (undef, $bitrate, 0, '', $ct, $length); +} + +sub _getBitrate { + my $ct = shift || ''; + + return 800_000 if $ct =~ /fla?c/; + return 320_000 if $ct =~ /aac|mp4/; + + return 256_00; } # URL used for CLI trackinfo queries @@ -276,12 +331,12 @@ sub trackInfoURL { my ( $class, $client, $url ) = @_; my ($trackId) = _getStreamParams( $url ); - + # SN URL to fetch track info menu my $trackInfoURL = Slim::Networking::SqueezeNetwork->url( '/api/wimp/v1/opml/trackinfo?trackId=' . $trackId ); - + return $trackInfoURL; } @@ -289,10 +344,10 @@ sub trackInfoURL { =pod XXX - legacy track info menu from before Slim::Menu::TrackInfo times? sub trackInfo { my ( $class, $client, $track ) = @_; - + my $url = $track->url; my $trackInfoURL = $class->trackInfoURL( $client, $url ); - + # let XMLBrowser handle all our display my %params = ( header => 'PLUGIN_WIMP_GETTING_TRACK_DETAILS', @@ -300,11 +355,11 @@ sub trackInfo { title => Slim::Music::Info::getCurrentTitle( $client, $url ), url => $trackInfoURL, ); - + main::DEBUGLOG && $log->debug( "Getting track information for $url" ); Slim::Buttons::Common::pushMode( $client, 'xmlbrowser', \%params ); - + $client->modeParam( 'handledTransition', 1 ); } =cut @@ -312,18 +367,18 @@ sub trackInfo { # Metadata for a URL, used by CLI/JSON clients sub getMetadataFor { my ( $class, $client, $url ) = @_; - + my $icon = $class->getIcon(); - + return {} unless $url; - + my $cache = Slim::Utils::Cache->new; - + # If metadata is not here, fetch it so the next poll will include the data my ($trackId, $format) = _getStreamParams( $url ); my $meta = $cache->get( 'wimp_meta_' . ($trackId || '') ); - - if ( !$meta && !$client->master->pluginData('fetchingMeta') ) { + + if ( !($meta && $meta->{duration}) && !$client->master->pluginData('fetchingMeta') ) { $client->master->pluginData( fetchingMeta => 1 ); @@ -331,25 +386,26 @@ sub getMetadataFor { my %need = ( $trackId => 1 ); - + for my $track ( @{ Slim::Player::Playlist::playList($client) } ) { my $trackURL = blessed($track) ? $track->url : $track; if ( my ($id) = _getStreamParams( $trackURL ) ) { - if ( $id && !$cache->get("wimp_meta_$id") ) { + my $cached = $id && $cache->get("wimp_meta_$id"); + if ( $id && !($cached && $cached->{duration}) ) { $need{$id}++; } } } - + if (keys %need) { if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Need to fetch metadata for: " . join( ', ', keys %need ) ); } - + my $metaUrl = Slim::Networking::SqueezeNetwork->url( "/api/wimp/v1/playback/getBulkMetadata" ); - + my $http = Slim::Networking::SqueezeNetwork->new( \&_gotBulkMetadata, \&_gotBulkMetadataError, @@ -358,7 +414,7 @@ sub getMetadataFor { timeout => 60, }, ); - + $http->post( $metaUrl, 'Content-Type' => 'application/x-www-form-urlencoded', @@ -369,12 +425,13 @@ sub getMetadataFor { $client->master->pluginData( fetchingMeta => 0 ); } } - + #$log->debug( "Returning metadata for: $url" . ($meta ? '' : ': default') ); - + $meta->{cover} ||= $meta->{icon} ||= $icon; + return $meta || { - bitrate => '256k CBR', - type => 'MP3', + bitrate => '320k CBR', + type => 'AAC', icon => $icon, cover => $icon, }; @@ -383,40 +440,40 @@ sub getMetadataFor { sub _gotBulkMetadata { my $http = shift; my $client = $http->params->{client}; - + $client->master->pluginData( fetchingMeta => 0 ); - + my $info = eval { from_json( $http->content ) }; - + if ( $@ || ref $info ne 'ARRAY' ) { $log->error( "Error fetching track metadata: " . ( $@ || 'Invalid JSON response' ) ); return; } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Caching metadata for " . scalar( @{$info} ) . " tracks" ); } - + # Cache metadata my $cache = Slim::Utils::Cache->new; my $icon = Slim::Plugin::WiMP::Plugin->_pluginDataFor('icon'); for my $track ( @{$info} ) { next unless ref $track eq 'HASH'; - + # cache the metadata we need for display my $trackId = delete $track->{id}; - + if ( !$track->{cover} ) { $track->{cover} = $icon; } my $bitrate = delete($track->{bitrate}); - + my $meta = { %{$track}, bitrate => $bitrate*1 > 320 ? 'PCM VBR ' : ($bitrate . 'k CBR'), - type => $bitrate*1 > 320 ? 'FLAC' : 'MP3', + type => $bitrate*1 > 320 ? 'FLAC' : ($bitrate*1 > 256 ? 'AAC' : 'MP3'), info_link => 'plugins/wimp/trackinfo.html', icon => $icon, }; @@ -425,10 +482,10 @@ sub _gotBulkMetadata { # if we didn't cache at all, we'd keep on hammering our servers $cache->set( 'wimp_meta_' . $trackId, $meta, $bitrate ? 86400 : 500 ); } - + # Update the playlist time so the web will refresh, etc $client->currentPlaylistUpdateTime( Time::HiRes::time() ); - + Slim::Control::Request::notifyFromArray( $client, [ 'newmetadata' ] ); } @@ -436,9 +493,9 @@ sub _gotBulkMetadataError { my $http = shift; my $client = $http->params('client'); my $error = $http->error; - + $client->master->pluginData( fetchingMeta => 0 ); - + $log->warn("Error getting track metadata from SN: $error"); } @@ -449,7 +506,7 @@ sub getIcon { } sub _getStreamParams { - if ( $_[0] =~ m{wimp://(.+)\.(m4a|aac|mp3|flac)}i ) { + if ( $_[0] =~ m{wimp://(.+)\.(m4a|aac|mp3|mp4|flac)}i ) { return ($1, lc($2) ); } } diff --git a/Slim/Plugin/WiMP/strings.txt b/Slim/Plugin/WiMP/strings.txt index 24ffd83c4b5..a531d181845 100644 --- a/Slim/Plugin/WiMP/strings.txt +++ b/Slim/Plugin/WiMP/strings.txt @@ -2,7 +2,7 @@ PLUGIN_WIMP_MODULE_NAME EN TIDAL NL TIDAL NO TIDAL - + PLUGIN_WIMP_MODULE_DESC EN TIDAL - High Fidelity Music Streaming NL TIDAL - High Fidelity Music Streaming @@ -62,7 +62,7 @@ PLUGIN_WIMP_ON_WIMP FI TIDAL:ssä FR Sur TIDAL IT Su TIDAL - NL Over TIDAL + NL Op TIDAL NO På TIDAL PL W usłudze TIDAL RU На TIDAL diff --git a/Slim/Plugin/iTunes/Common.pm b/Slim/Plugin/iTunes/Common.pm index f44847f7a62..e266c2b781c 100644 --- a/Slim/Plugin/iTunes/Common.pm +++ b/Slim/Plugin/iTunes/Common.pm @@ -1,6 +1,6 @@ package Slim::Plugin::iTunes::Common; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/iTunes/Importer.pm b/Slim/Plugin/iTunes/Importer.pm index 168f2eeab44..505b565b9d4 100644 --- a/Slim/Plugin/iTunes/Importer.pm +++ b/Slim/Plugin/iTunes/Importer.pm @@ -1,6 +1,6 @@ package Slim::Plugin::iTunes::Importer; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/iTunes/Plugin.pm b/Slim/Plugin/iTunes/Plugin.pm index 3ca3b5ba67a..8352757d293 100644 --- a/Slim/Plugin/iTunes/Plugin.pm +++ b/Slim/Plugin/iTunes/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::iTunes::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/iTunes/Settings.pm b/Slim/Plugin/iTunes/Settings.pm index 0a6766a9972..d0cb79f64e8 100644 --- a/Slim/Plugin/iTunes/Settings.pm +++ b/Slim/Plugin/iTunes/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::iTunes::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/iTunes/strings.txt b/Slim/Plugin/iTunes/strings.txt index b51e8a7be0c..05c10dfeaa9 100644 --- a/Slim/Plugin/iTunes/strings.txt +++ b/Slim/Plugin/iTunes/strings.txt @@ -1,7 +1,5 @@ # String file for iTunes plugin -# $Id: strings.txt 11145 2007-01-07 00:19:08Z kdf $ - ITUNES CS iTunes DA iTunes @@ -62,7 +60,7 @@ SETUP_ITUNESPLAYLISTFORMAT_DESC HE אפשרות זו קובעת את התבנית שבה נעשה שימוש בעת ייבוא רשימות השמעה של iTunes. IT Questa opzione consente di specificare il formato utilizzato per l'importazione delle playlist di iTunes. JA このオプションでは、iTunesプレイリストをインポートする時に使われるフォーマットを決めます。 - NL Deze optie bepaalt welk formaat bij het importeren van iTunes-playlists gebruikt wordt. + NL Bepaal hier welk formaat gebruikt moet worden bij het importeren van iTunes-playlists. NO Denne innstillingen avgjør hvilket format som brukes ved importering av iTunes-spillelister. PL Ta opcja określa format używany podczas importowania list odtwarzania programu iTunes. RU Этот параметр определяет формат для импорта плей-листов iTunes. @@ -135,7 +133,7 @@ SETUP_ITUNES_DESC HE Logitech Media Server יכול להשתמש בספריית המוסיקה וברשימות ההשמעה של iTunes. כל שינוי שתבצע ב-iTunes ייוצג בנגן. סמן את התיבה שלהלן אם ברצונך לייבא את מידע ספריית המוסיקה של iTunes. IT In Logitech Media Server è possibile utilizzare la libreria musicale e le playlist di iTunes. Le eventuali modifiche apportate in iTunes vengono estese al lettore. Selezionare la casella seguente se si desidera importare i dati della libreria musicale di iTunes. JA Logitech Media ServerはiTunesの音楽ライブラリーとプレイリストを使用することができます。iTunesにおける変更はプレーヤーに反映されます。iTunesの音楽ライブラリー情報を利用するには、以下の項目をチェックしてください。 - NL Logitech Media Server kan gebruik maken van je iTunes-muziekcollectie en -playlists. Alle veranderingen in iTunes worden ook op je muzieksysteem doorgevoerd. Selecteer het vakje hieronder als je de iTunes-muziekcollectiegegevens wilt importeren. + NL Logitech Media Server kan gebruik maken van je iTunes-muziekcollectie en -playlists. Alle wijzigingen in iTunes worden ook op je muziekspeler doorgevoerd. Selecteer het vakje hieronder als je de iTunes-muziekcollectiegegevens wilt importeren. NO Logitech Media Server kan bruke musikkbiblioteket og spillelistene til iTunes. Hvis du endrer noe i iTunes, vises disse endringene på spilleren. Merk av i boksen nedenfor hvis du vil importere musikkbibliotekinformasjonen til iTunes. PL Program Logitech Media Server może korzystać z biblioteki muzyki i list odtwarzania programu iTunes. Wszelki zmiany wprowadzone w programie iTunes zostaną uwzględnione w odtwarzaczu. Aby zaimportować informacje z biblioteki muzyki programu iTunes, zaznacz pole wyboru poniżej. RU Logitech Media Server может использовать медиатеку и плей-листы iTunes. Все изменения, вносимые в iTunes, отражаются в плеере. Установите флажок (ниже), чтобы импортировать данные медиатеки iTunes. @@ -170,7 +168,7 @@ SETUP_ITUNESSCANINTERVAL_DESC FR Le Logitech Media Server importe automatiquement les informations de la bibliothèque musicale d'iTunes lorsque vous la modifiez. Vous pouvez spécifier le délai minimal en secondes au bout duquel le serveur actualise la bibliothèque musicale d'iTunes. La valeur 0 désactive cette fonctionnalité. HE כאשר ספריית המוסיקה של iTunes משתנה, Logitech Media Server מייבא את מידע הספרייה באופן אוטומטי. באפשרותך לציין כמות זמן מינימלית (בשניות) שבמהלכה על Logitech Media Server להמתין לפני טעינה חוזרת של ספריית המוסיקה של iTunes. ערך אפס משבית את אפשרות הטעינה מחדש. IT Se si modifica la libreria musicale di iTunes, Logitech Media Server importa automaticamente i nuovi dati. È possibile specificare il periodo minimo (in secondi) che il server deve attendere prima di ricaricare la libreria musicale di iTunes. Se si imposta il valore zero, la libreria non viene ricaricata. - NL Wanneer je iTunes-muziekcollectie verandert, worden de collectiegegevens automatisch door Logitech Media Server geïmporteerd. Je kunt opgeven (in seconden) hoe lang de server minimaal moet wachten alvorens je iTunes-muziekcollectie te herladen. Een waarde van 0 seconden schakelt herladen uit. + NL Wanneer je iTunes-muziekcollectie wijzigt, worden de collectiegegevens automatisch door Logitech Media Server geïmporteerd. Je kunt opgeven (in seconden) hoe lang de server minimaal moet wachten alvorens je iTunes-muziekcollectie te herladen. Een waarde van 0 seconden schakelt herladen uit. NO Når musikkbiblioteket til iTunes endres, importerer Logitech Media Server automatisk bibliotekinformasjonen. Du kan angi en minimumstid (i sekunder) for hvor lenge serveren skal vente med å laste inn musikkbiblioteket til iTunes igjen. Hvis verdien er null, deaktiveres innlasting. PL Jeżeli biblioteka muzyki programu iTunes zmieni się, program Logitech Media Server automatycznie zaimportuje informacje z biblioteki. Możliwe jest określenie minimalnego czasu (w sekundach) po jakim program Logitech Media Server ponownie załaduje bibliotekę muzyki programu iTunes. Ustawienie wartości zero powoduje wyłączenie ponownego ładowania. RU При изменении медиатеки iTunes ее данные автоматически импортируются в Logitech Media Server. Для сервера можно задать минимальный интервал времени (в секундах) перед повторной загрузкой медиатеки iTunes. Значение 0 означает отмену повторной загрузки данных. @@ -277,7 +275,7 @@ SETUP_IGNOREDISABLEDITUNESTRACKS_DESC FR Par défaut, lorsque vous désélectionnez un morceau dans iTunes, celui-ci ne s'affiche pas lorsque vous parcourez votre musique grâce au Logitech Media Server. Vous pouvez modifier ce comportement ci-dessous. HE כברירת מחדל, כאשר אתה מסיר את סימן הביקורת שמוצב ליד שיר ב-iTunes, השיר לא יופיע בעת עיון בפריטי המוסיקה באמצעות Logitech Media Server. באפשרותך לשנות את ההתנהגות בהגדרות שלהלן. IT Per impostazione predefinita, se in iTunes si deseleziona la casella in corrispondenza di un brano, tale brano non viene visualizzato quando si sfoglia la raccolta musicale con Logitech Media Server. È possibile modificare questa impostazione di seguito. - NL Wanneer je het vinkje weghaalt naast een nummer in iTunes, wordt dat nummer standaard niet weergegeven wanneer je met Logitech Media Server door je muziek bladert. Je kunt dit hieronder veranderen. + NL Standaard worden nummers die in iTunes niet aangevinkt zijn, niet in Logitech Media Server weergegeven. Je kunt dit hieronder wijzigen. NO Hvis du fjerner markeringen ved en sang i iTunes, vises den som standard ikke når du blar i musikk med Logitech Media Server. Du kan endre dette nedenfor. PL Domyślnie po usunięciu zaznaczenia pola wyboru obok utworu w programie iTunes utwór ten nie będzie wyświetlany podczas przeglądania muzyki w programie Logitech Media Server. Działanie tej funkcji można zmienić poniżej. RU По умолчанию, если снять флажок песни в iTunes, она не будет отображаться при просмотре в Logitech Media Server. Изменить эту настройку можно ниже. @@ -372,7 +370,7 @@ ITUNES_ARTWORK_PHASE_1_PROGRESS FI iTunesin lataaman kansitaiteen tuominen FR Importations de pochettes iTunes téléchargées IT Importazione copertine di iTunes scaricate - NL Import van door iTunes gedownloade hoesafbeeldingen + NL Import van door iTunes gedownloade albumcovers NO Importering av omslag lastet ned fra iTunes PL Importowanie okładek pobranych do programu iTunes RU Импорт обложек, загруженных в iTunes @@ -387,7 +385,7 @@ ITUNES_ARTWORK_PHASE_2_PROGRESS FI iTunesin käyttäjän lisäämän kansitaiteen tuominen FR Importations de pochettes iTunes ajoutées par l'utilisateur IT Importazione copertine di iTunes aggiunte dall'utente - NL Import van door iTunes-gebruiker toegevoegde hoesafbeeldingen + NL Import van door iTunes-gebruiker toegevoegde albumcovers NO Importering av omslag lagt til av bruker fra iTunes PL Użytkownik programu iTunes - importowanie dodanych okładek RU Импорт обложек, добавленных пользователем iTunes @@ -402,22 +400,23 @@ ITUNES_ARTWORK_PHASE_2_PROGRESS # "9000.038" = "Audiobooks"; # "9000.039" = "Purchased"; # "9000.043" = "Rented Movies"; +# "4zg6f8rszh" = "Downloaded"; ITUNES_IGNORED_PLAYLISTS_DEFAULTS # SLT: These terms MUST correspond to the translation used in iTunes, or the filter will fail - CS Knihovna, videa, filmy, televizní show, hudba, koupené, půjčené filmy - DA Bibliotek, Videoer, Film, Tv-udsendelser, Musik, Indkøb, Lejede film - DE Mediathek, Videos, Filme, Fernsehsendungen, Musik, Einkäufe, Ausgeliehene Filme - EN Library, Videos, Movies, TV Shows, Music, Purchased, Rented Movies - ES Biblioteca, Vídeos, Películas, Programas de televisión, Música, Comprado, Películas alquiladas - FI Kirjasto, videot, elokuvat, tv-ohjelmat, musiikki, ostetut, vuokraelokuvat - FR Bibliothèque, Vidéos, Films, Séries TV, Musique, Achats, Films en location - IT Libreria, Video, Filmati, Spettacoli TV, Musica, Acquisti, Filmati noleggiati - NL Bibliotheek, Video's, Films, Tv-programma's, Muziek, Aangeschaft, Gehuurde films - NO Bibliotek, Videoer, Filmer, TV-programmer, Musikk, Kjøpt, Leide filmer - PL Biblioteka, Pliki wideo, Filmy, Programy telewizyjne, Muzyka, Zakupione, Wypożyczone filmy - RU Медиатека, Видео, Фильмы, Телешоу, Музыка, Приобретено, Взятые напрокат фильмы - SV Bibliotek, Videor, Filmer, TV-program, Musik, Köpt, Hyrfilmer + CS Knihovna, videa, filmy, televizní show, hudba, koupené, půjčené filmy, Staženo + DA Bibliotek, Videoer, Film, Tv-udsendelser, Musik, Indkøb, Lejede film, Hentet + DE Mediathek, Videos, Filme, Fernsehsendungen, Musik, Einkäufe, Ausgeliehene Filme, Geladen + EN Library, Videos, Movies, TV Shows, Music, Purchased, Rented Movies, Downloaded + ES Biblioteca, Vídeos, Películas, Programas de televisión, Música, Comprado, Películas alquiladas, Descargado + FI Kirjasto, videot, elokuvat, tv-ohjelmat, musiikki, ostetut, vuokraelokuvat, Ladattu + FR Bibliothèque, Vidéos, Films, Séries TV, Musique, Achats, Films en location, Téléchargé + IT Libreria, Video, Filmati, Spettacoli TV, Musica, Acquisti, Filmati noleggiati, Scaricati + NL Bibliotheek, Video's, Films, Tv-programma's, Muziek, Aangeschaft, Gehuurde films, Gedownload + NO Bibliotek, Videoer, Filmer, TV-programmer, Musikk, Kjøpt, Leide filmer, Lastet ned + PL Biblioteka, Pliki wideo, Filmy, Programy telewizyjne, Muzyka, Zakupione, Wypożyczone filmy, Pobrane + RU Медиатека, Видео, Фильмы, Телешоу, Музыка, Приобретено, Взятые напрокат фильмы, Загружено + SV Bibliotek, Videor, Filmer, TV-program, Musik, Köpt, Hyrfilmer, Hämtat SETUP_ITUNES_IGNORED_PLAYLISTS CS Ignorované seznamy skladeb @@ -488,7 +487,7 @@ SETUP_ITUNES_EXTRACT_ARTWORK FI Pura iTunesin kansikuvat (kokeellinen) FR Extraire les pochettes iTunes (expérimental) IT Estrai illustrazioni di iTunes (sperimentale) - NL iTunes-artwork extraheren (experiment) + NL iTunes-artwork extraheren (experimenteel) NO Hent albumomslag fra iTunes (eksperimentell) PL Wyodrębnij okładki programu iTunes (eksperymentalne) RU Извлечь обложку iTunes (экспериментально) diff --git a/Slim/Plugin/xPL/Plugin.pm b/Slim/Plugin/xPL/Plugin.pm index 32ec6a24abc..72be29f87be 100644 --- a/Slim/Plugin/xPL/Plugin.pm +++ b/Slim/Plugin/xPL/Plugin.pm @@ -1,6 +1,6 @@ package Slim::Plugin::xPL::Plugin; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -14,8 +14,6 @@ package Slim::Plugin::xPL::Plugin; # xPL Protocol Support Plugin for Logitech Media Server # http://www.xplproject.org.uk/ -# $Id: Plugin.pm 10841 2006-12-03 16:57:58Z adrian $ - use strict; use IO::Socket; use Scalar::Util qw(blessed); diff --git a/Slim/Plugin/xPL/Settings.pm b/Slim/Plugin/xPL/Settings.pm index 60b7322a224..e5055a4486d 100644 --- a/Slim/Plugin/xPL/Settings.pm +++ b/Slim/Plugin/xPL/Settings.pm @@ -1,6 +1,6 @@ package Slim::Plugin::xPL::Settings; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Plugin/xPL/strings.txt b/Slim/Plugin/xPL/strings.txt index 1fc7a472846..efbea8eed27 100644 --- a/Slim/Plugin/xPL/strings.txt +++ b/Slim/Plugin/xPL/strings.txt @@ -1,7 +1,5 @@ # String file for xPL plugin -# $Id: strings.txt 10821 2006-12-01 21:44:53Z adrian $ - PLUGIN_XPL CS Interface xPL DA xPL-grænseflade diff --git a/Slim/Schema.pm b/Slim/Schema.pm index ce80fbd9425..b40388e1af4 100644 --- a/Slim/Schema.pm +++ b/Slim/Schema.pm @@ -1,8 +1,7 @@ package Slim::Schema; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -124,16 +123,16 @@ Must be called before any other actions. Generally from L sub init { my ( $class, $dsn, $sql ) = @_; - + return if $initialized; - + my $dbh = $class->_connect($dsn, $sql) || do { # Not much we can do if there's no DB. logBacktrace("Couldn't connect to database! Fatal error: [$!] Exiting!"); exit; }; - + if (Slim::Utils::OSDetect->getOS()->sqlHelperClass()->canCacheDBHandle()) { $_dbh = $dbh; } @@ -146,14 +145,11 @@ sub init { eval { local $dbh->{HandleError} = sub {}; $dbh->do('SELECT name FROM metainformation') || die $dbh->errstr; - + # when upgrading from SBS to LMS let's check the additional tables, # as the schema numbers might be overlapping, not causing a re-build $dbh->do('SELECT id FROM images LIMIT 1') || die $dbh->errstr; $dbh->do('SELECT id FROM videos LIMIT 1') || die $dbh->errstr; - - # always reset the isScanning flag upon restart - Slim::Utils::OSDetect::isSqueezeOS() && $dbh->do("UPDATE metainformation SET value = '0' WHERE name = 'isScanning'"); }; # If we couldn't select our new 'name' column, then drop the @@ -203,7 +199,7 @@ sub init { } $trackAttrs = Slim::Schema::Track->attributes; - + if ( main::STATISTICS ) { $trackPersistentAttrs = Slim::Schema::TrackPersistent->attributes; } @@ -212,7 +208,7 @@ sub init { $class->storage->debugobj('Slim::Schema::Debug'); $class->updateDebug; - + # Bug 17609, avoid a possible locking issue by ensuring VA object is up to date at init time # instead of waiting until the first time it's called, for example through artistsQuery. $class->variousArtistsObject; @@ -245,7 +241,7 @@ sub init { $prefs->set('migratedMovCT' => 1); } - + if ( !main::SCANNER ) { # Wipe cached data after rescan Slim::Control::Request::subscribe( sub { @@ -262,18 +258,18 @@ sub hasLibrary { sub _connect { my ( $class, $dsn, $sql ) = @_; - + $sql ||= []; - + my ($driver, $source, $username, $password) = $class->sourceInformation; # For custom exceptions $class->storage_type('Slim::Schema::Storage'); - + my $sqlHelperClass = Slim::Utils::OSDetect->getOS()->sqlHelperClass(); my $on_connect_do = $sqlHelperClass->on_connect_do(); - - $class->connection( $dsn || $source, $username, $password, { + + $class->connection( $dsn || $source, $username, $password, { RaiseError => 1, AutoCommit => 1, PrintError => 0, @@ -283,9 +279,9 @@ sub _connect { @{$sql}, ] } ) || return; - + $sqlHelperClass->postConnect( $class->storage->dbh ); - + return $class->storage->dbh; } @@ -311,10 +307,10 @@ Debugging is normally disabled, but must be enabled if either logging for databa sub updateDebug { my $class = shift; - + # May not have a DB return if !hasLibrary(); - + my $debug = (main::INFOLOG && logger('database.sql')->is_info) || main::PERFMON; $class->storage->debug($debug); @@ -334,7 +330,7 @@ sub disconnect { $initialized = 0; } -=head2 sourceInformation() +=head2 sourceInformation() Returns in order: database driver name, DBI DSN string, username, password from the current settings. @@ -345,17 +341,17 @@ sub sourceInformation { my $class = shift; my $sqlHelperClass = Slim::Utils::OSDetect->getOS()->sqlHelperClass(); - + my $source = $sqlHelperClass->source(); my $username = $prefs->get('dbusername'); my $password = $prefs->get('dbpassword'); - + my ($driver) = ($source =~ /^dbi:(\w+):/); return ($driver, $source, $username, $password); } -=head2 wipeDB() +=head2 wipeDB() Wipes and reinitializes the database schema. Calls the schema_clear.sql script for the current database driver. @@ -366,14 +362,14 @@ WARNING - All data in the database will be dropped! sub wipeDB { my $class = shift; - + my $log = logger('scan.import'); main::INFOLOG && $log->is_info && $log->info("Start schema_clear"); my ($driver) = $class->sourceInformation; - eval { + eval { Slim::Utils::SQLHelper->executeSQLFile( $driver, $class->storage->dbh, "schema_clear.sql" ); @@ -384,7 +380,7 @@ sub wipeDB { if ($@) { logError("Failed to clear & migrate schema: [$@]"); } - + main::INFOLOG && $log->is_info && $log->info("End schema_clear"); } @@ -396,17 +392,17 @@ Calls the schema_optimize.sql script for the current database driver. sub optimizeDB { my $class = shift; - + my $log = logger('scan.import'); main::INFOLOG && $log->is_info && $log->info("Start schema_optimize"); my ($driver) = $class->sourceInformation; - my $progress = Slim::Utils::Progress->new({ - 'type' => 'importer', - 'name' => 'dboptimize', - 'total' => 2, + my $progress = Slim::Utils::Progress->new({ + 'type' => 'importer', + 'name' => 'dboptimize', + 'total' => 2, 'bar' => 1 }); @@ -414,7 +410,7 @@ sub optimizeDB { Slim::Utils::SQLHelper->executeSQLFile( $driver, $class->storage->dbh, "schema_optimize.sql" ); - + $progress->update(); $class->forceCommit; @@ -438,12 +434,12 @@ sub optimizeDB { AND idx NOT LIKE 'sqlite_auto%' ORDER BY tbl } ); - + my ($idx, $stats); - + $stats_sth->execute; $stats_sth->bind_columns( \$idx, \$stats ); - + while ( $stats_sth->fetch ) { $log->error( sprintf('%30s: %s', $idx, $stats) ); } @@ -465,18 +461,18 @@ sub migrateDB { # Migrate to the latest schema version - see SQL/$driver/schema_\d+_up.sql my $dbix = DBIx::Migration->new({ - dbh => $dbh, + dbh => $dbh, dir => catdir(Slim::Utils::OSDetect::dirsFor('SQL'), $driver), debug => $log->is_debug, }); - + # Hide errors that aren't really errors my $cur_handler = $dbh->{HandleError}; my $new_handler = sub { return 1 if $_[0] =~ /no such table/; goto $cur_handler; }; - + local $dbh->{HandleError} = $new_handler; my $old = $dbix->version || 0; @@ -533,12 +529,12 @@ Returns a L for the specified class. A shortcut for resultset() -=cut +=cut sub rs { my $class = shift; my $rsClass = ucfirst shift; - + if ( !exists $RS_CACHE{$rsClass} ) { $RS_CACHE{$rsClass} = $class->resultset($rsClass); } @@ -552,7 +548,7 @@ Returns a L for the specified class. A shortcut for resultset($class)->search($cond, $attr) -=cut +=cut sub search { my $class = shift; @@ -567,7 +563,7 @@ Returns a single result from a search on the specified class' Lsingle($cond) -=cut +=cut sub single { my $class = shift; @@ -582,7 +578,7 @@ Returns the count result from a search on the specified class' Lcount($cond, $attr) -=cut +=cut sub count { my $class = shift; @@ -601,18 +597,18 @@ before returning. Overrides L -=cut +=cut sub find { my $class = shift; my $rsClass = ucfirst(shift); - + # If we only have a single attribute and it is not a reference and it is negative # then this indicates a remote track. if (@_ == 1 && ! ref $_[0] && $_[0] < 0) { return Slim::Schema::RemoteTrack->fetchById($_[0]); } - + return if !$initialized; my $object = eval { $class->rs($rsClass)->find(@_) }; @@ -647,7 +643,7 @@ sub searchTypes { return qw(contributor album genre track); } -=head2 contentType( $urlOrObj ) +=head2 contentType( $urlOrObj ) Fetch the content type for a URL or Track Object. @@ -709,7 +705,7 @@ sub contentType { if ((!defined $contentType || $contentType eq $defaultType) && $blessed) { $contentType = Slim::Music::Info::typeFromPath($url); - } + } # Only set the cache if we have a valid contentType if (defined $contentType && $contentType ne $defaultType) { @@ -738,7 +734,7 @@ Required $args: =over 4 -=item * +=item * The URL to look for. @@ -811,7 +807,7 @@ sub objectForUrl { if (!$url) { - logBacktrace("Null track request! Returning undef."); + logBacktrace("Null track request! Returning undef."); return undef; } @@ -822,14 +818,14 @@ sub objectForUrl { # Pull the track object for the DB my $track = $self->_retrieveTrack($url, $playlist); - + # Bug 14648: Check to see if we have a playlist with remote tracks if (!$track && defined $playlistId && Slim::Music::Info::isRemoteURL($url)) { if (my $playlistObj = $self->find('Playlist', $playlistId)) { # Parse the playlist file to cause the RemoteTrack objects to be created Slim::Formats::Playlists->parseList($playlistObj->url); - + # try again $track = $self->_retrieveTrack($url, $playlist); } @@ -856,24 +852,24 @@ sub objectForUrl { sub _createOrUpdateAlbum { my ($self, $attributes, $trackColumns, $isCompilation, $contributorId, $hasAlbumArtist, $create, $track, $basename) = @_; - + my $dbh = $self->dbh; - + # Now handle Album creation my $title = $attributes->{ALBUM}; my $disc = $attributes->{DISC}; my $discc = $attributes->{DISCC}; # Bug 10583 - Also check for MusicBrainz Album Id my $brainzId = $attributes->{MUSICBRAINZ_ALBUM_ID}; - + my $isDebug = main::DEBUGLOG && $log->is_debug; - + # Bug 17322, strip leading/trailing spaces from name if ( $title ) { $title =~ s/^ +//; $title =~ s/ +$//; } - + # Bug 4361, Some programs (iTunes) tag things as Disc 1/1, but # we want to ignore that or the group discs logic below gets confused # Bug 10583 - Revert disc 1/1 change. @@ -885,16 +881,16 @@ sub _createOrUpdateAlbum { # $log->debug( '-- Ignoring useless DISCC tag value of 1' ); # $disc = $discc = undef; #} - + my $albumId; my $albumHash = {}; - + if ($track && !$trackColumns) { $trackColumns = { $track->get_columns }; } my $noAlbum = string('NO_ALBUM'); - + if ( !$create && $track ) { $albumHash = Slim::Schema::Album->findhash( $track->album->id ); @@ -905,20 +901,20 @@ sub _createOrUpdateAlbum { $create = 1; } } - + # If the album does not have a title, use the singleton "No Album" album - if ( $create && !$title ) { - # let the external scanner make an attempt to find any existing "No Album" in the + if ( $create && (!defined $title || $title eq '') ) { + # let the external scanner make an attempt to find any existing "No Album" in the # database before we assume there are none from previous scans if ( !defined $_unknownAlbumId ) { $_unknownAlbumId = $dbh->selectrow_array( qq{ SELECT id FROM albums WHERE title = ? }, undef, $noAlbum ); } - + if ( !defined $_unknownAlbumId ) { my $sortkey = Slim::Utils::Text::ignoreCaseArticles($noAlbum); - + $albumHash = { title => $noAlbum, titlesort => $sortkey, @@ -927,14 +923,14 @@ sub _createOrUpdateAlbum { year => 0, contributor => $vaObjId || $self->variousArtistsObject->id, }; - + $_unknownAlbumId = $self->_insertHash( albums => $albumHash ); main::DEBUGLOG && $isDebug && $log->debug(sprintf("-- Created NO ALBUM as id: [%d]", $_unknownAlbumId)); } else { # Bug 17370, detect if No Album is a "compilation" (more than 1 artist with No Album) - # We have to check the other tracks already on this album, and if the artists differ + # We have to check the other tracks already on this album, and if the artists differ # from the current track's artists, we have a compilation my $is_comp = $self->mergeSingleVAAlbum( $_unknownAlbumId, 1 ); @@ -947,13 +943,13 @@ sub _createOrUpdateAlbum { } main::DEBUGLOG && $isDebug && $log->debug("-- Track has no album"); - + return $_unknownAlbumId; } - + # Used for keeping track of the album name. $basename ||= dirname($trackColumns->{'url'}); - + if ($create) { # Calculate once if we need/want to test for disc @@ -962,7 +958,7 @@ sub _createOrUpdateAlbum { my $checkDisc = 0; # Bug 10583 - Revert disc 1/1 change. Use MB Album Id in addition (unique id per disc, not per set!) - if (!$prefs->get('groupdiscs') && + if (!$prefs->get('groupdiscs') && (($disc && $discc) || ($disc && !$discc) || $brainzId)) { $checkDisc = 1; @@ -982,7 +978,7 @@ sub _createOrUpdateAlbum { # get() doesn't run the UTF-8 trigger, and ->title() calls # Slim::Schema::Album->title() which has different behavior. - if ( + if ( $lastAlbum->{_dirname} && $lastAlbum->{_dirname} eq $basename && $lastAlbum->{title} eq $title @@ -998,19 +994,25 @@ sub _createOrUpdateAlbum { my $search = []; my $values = []; my $join; - + # Don't use year as a search criteria. Compilations in particular # may have different dates for each track... # If re-added here then it should be checked also above, otherwise # the server behaviour changes depending on the track order! # Maybe we need a preference? # This used to do: #'year' => $trackColumns{'year'}, - + push @{$search}, 'albums.title = ?'; push @{$values}, $title; + if (defined $brainzId) { + push @{$search}, 'albums.musicbrainz_id = ?'; + push @{$values}, $brainzId; + main::DEBUGLOG && $isDebug && $log->debug(sprintf("-- Checking for MusicBrainz Album Id: %s", $brainzId)); + } + my $checkContributor; - + # Add disc to the search criteria if needed if ($checkDisc) { if ($disc) { @@ -1018,14 +1020,6 @@ sub _createOrUpdateAlbum { push @{$values}, $disc; } - # Bug 10583 - Also check musicbrainz_id if defined. - # Can't be used in groupdiscs mode since id is unique per disc, not per set. - if (defined $brainzId) { - push @{$search}, 'albums.musicbrainz_id = ?'; - push @{$values}, $brainzId; - main::DEBUGLOG && $isDebug && $log->debug(sprintf("-- Checking for MusicBrainz Album Id: %s", $brainzId)); - } - $checkContributor = 1; } elsif ($discc) { @@ -1033,10 +1027,10 @@ sub _createOrUpdateAlbum { # groupdiscs mode, check discc if it exists, # in the case where there are multiple albums # of the same name by the same artist. bug3254 - + push @{$search}, 'albums.discc = ?'; push @{$values}, $discc; - + $checkContributor = 1; } elsif ( defined $disc && !defined $discc ) { @@ -1045,10 +1039,10 @@ sub _createOrUpdateAlbum { # albums of the same name, but one is # multidisc _without_ having a discc set. push @{$search}, 'albums.disc IS NOT NULL'; - + $checkContributor = 1; } - + if ( $checkContributor && defined $contributorId ) { # Bug 4361, also match on contributor, so we don't group # different multi-disc albums together just because they @@ -1057,7 +1051,7 @@ sub _createOrUpdateAlbum { if ( $isCompilation && !$hasAlbumArtist ) { $contributor = $self->variousArtistsObject->id; } - + push @{$search}, 'albums.contributor = ?'; push @{$values}, $contributor; } @@ -1066,7 +1060,7 @@ sub _createOrUpdateAlbum { # values are undefined. if ( !defined $disc ) { push @{$search}, 'albums.disc IS NULL'; - + if ( !defined $discc ) { push @{$search}, 'albums.discc IS NULL'; } @@ -1104,24 +1098,24 @@ sub _createOrUpdateAlbum { push @{$values}, "$basename%"; $join = 1; } - + main::DEBUGLOG && $isDebug && $log->debug( "-- Searching for an album with: " . Data::Dump::dump($search, $values) ); - + my $sql = 'SELECT albums.* FROM albums '; $sql .= 'JOIN tracks ON (albums.id = tracks.album) ' if $join; $sql .= 'WHERE '; $sql .= join( ' AND ', @{$search} ); $sql .= ' LIMIT 1'; - + my $sth = $dbh->prepare_cached($sql); $sth->execute( @{$values} ); - + $albumHash = $sth->fetchrow_hashref || {}; - + $sth->finish; - + main::DEBUGLOG && $isDebug && $albumHash->{id} && $log->debug(sprintf("-- Found the album id: [%d]", $albumHash->{id})); - + # We've found an album above - and we're not looking # for a multi-disc or compilation album; check to see # if that album already has a track number that @@ -1137,11 +1131,11 @@ sub _createOrUpdateAlbum { AND tracknum = ? LIMIT 1 } ); - + $sth->execute( $albumHash->{id}, $trackColumns->{tracknum} ); my ($matchTrack) = $sth->fetchrow_array; $sth->finish; - + if ( $matchTrack && dirname($matchTrack) ne $basename ) { main::INFOLOG && $log->is_info && $log->info(sprintf("-- Track number mismatch with album id: [%d]", $albumHash->{id})); $albumHash = {}; @@ -1154,7 +1148,7 @@ sub _createOrUpdateAlbum { } } } - + # Always normalize the sort, as ALBUMSORT could come from a TSOA tag. $albumHash->{titlesort} = Slim::Utils::Text::ignoreCaseArticles( $attributes->{ALBUMSORT} || $title ); @@ -1166,13 +1160,13 @@ sub _createOrUpdateAlbum { # Bug 3255 - add album contributor which is either VA or the primary artist, used for sort by artist my $vaObjId = $vaObjId || $self->variousArtistsObject->id; - + if ( $isCompilation && !$hasAlbumArtist ) { $albumHash->{contributor} = $vaObjId } elsif ( defined $contributorId ) { $albumHash->{contributor} = $contributorId; - + # Set compilation to 1 if the primary contributor is VA if ( $contributorId == $vaObjId ) { $albumHash->{compilation} = 1; @@ -1185,7 +1179,7 @@ sub _createOrUpdateAlbum { for my $gainTag ( qw(REPLAYGAIN_ALBUM_GAIN REPLAYGAIN_ALBUM_PEAK) ) { my $shortTag = lc($gainTag); $shortTag =~ s/^replaygain_album_(\w+)$/replay_$1/; - + # Bug 8034, this used to not change gain/peak values if they were already set, # bug we do want to update album gain tags if they are changed. if ( $attributes->{$gainTag} ) { @@ -1194,7 +1188,7 @@ sub _createOrUpdateAlbum { $attributes->{$gainTag} =~ s/,/\./g; # bug 6900, change comma to period $albumHash->{$shortTag} = $attributes->{$gainTag}; - + # Bug 15483, remove non-numeric gain tags if ( $albumHash->{$shortTag} !~ /^[\d\-\+\.]+$/ ) { my $file = Slim::Utils::Misc::pathFromFileURL($trackColumns->{url}); @@ -1239,7 +1233,7 @@ sub _createOrUpdateAlbum { else { $albumHash->{year} = undef; } - + # Bug 7731, filter out duplicate keys that end up as array refs while ( my ($tag, $value) = each %{$albumHash} ) { if ( ref $value eq 'ARRAY' ) { @@ -1251,8 +1245,8 @@ sub _createOrUpdateAlbum { # Update the album title - the user might have changed it. $albumHash->{title} = $title; } - - # Link album cover to track cover + + # Link album cover to track cover # Future TODO: if an album has multiple images i.e. Ghosts, # prefer cover.jpg instead of embedded artwork for album? # Would require an additional cover column in the albums table @@ -1272,30 +1266,30 @@ sub _createOrUpdateAlbum { $log->debug("--- $tag : $value") if defined $value; } } - + # Detect if this album is a compilation when an explicit compilation tag is not available - # This takes the place of the old mergeVariousArtists method + # This takes the place of the old mergeVariousArtists method if ( !defined $isCompilation && $albumHash->{id} ) { - # We have to check the other tracks already on this album, and if the artists differ + # We have to check the other tracks already on this album, and if the artists differ # from the current track's artists, we have a compilation my $is_comp = $self->mergeSingleVAAlbum( $albumHash->{id}, 1 ); - + if ( $is_comp ) { $albumHash->{compilation} = 1; $albumHash->{contributor} = $vaObjId || $self->variousArtistsObject->id; - + main::DEBUGLOG && $isDebug && $log->debug( "Is a Comp : " . $albumHash->{title} ); } else { $albumHash->{compilation} = 0; - + main::DEBUGLOG && $isDebug && $log->debug( "Not a Comp : " . $albumHash->{title} ); } } - + # Bug: 3911 - don't add years for tracks without albums. $self->_createYear( $albumHash->{year} ); - + # create/update album if ( $albumHash->{id} ) { # Update the existing album @@ -1304,17 +1298,17 @@ sub _createOrUpdateAlbum { else { # Create a new album $albumHash->{id} = $self->_insertHash( albums => $albumHash ); - + main::DEBUGLOG && $isDebug && $log->debug(sprintf("-- Created album (id: [%d])", $albumHash->{id})); } - + # Just cache some stuff about the last Album so we can find it # again cheaply when we add the next track. # This really does away with lastTrack needing to be a hash # but perhaps this should be a dirname-indexed hash instead, # perhaps even LRU, although LRU is surprisingly costly. # This depends on whether we need to cope with out-of-order scans - # and I don't really know. + # and I don't really know. $lastAlbum = $albumHash; $lastAlbum->{_dirname} = $basename; @@ -1324,21 +1318,21 @@ sub _createOrUpdateAlbum { # Years have their own lookup table. sub _createYear { my ($self, $year) = @_; - + if (defined $year) { # Bug 17322, strip leading/trailing spaces from name $year =~ s/^ +//; $year =~ s/ +$//; - + if ($year =~ /^\d+$/) { # Using native DBI here to improve performance during scanning my $dbh = Slim::Schema->dbh; - + my $sth = $dbh->prepare_cached('SELECT 1 FROM years WHERE id = ?'); $sth->execute($year); my ($exists) = $sth->fetchrow_array; $sth->finish; - + if ( !$exists ) { $sth = $dbh->prepare_cached( 'INSERT INTO years (id) VALUES (?)' ); $sth->execute($year); @@ -1348,11 +1342,11 @@ sub _createYear { } sub _createComments { my ($self, $comments, $trackId) = @_; - + if ( $comments ) { # Using native DBI here to improve performance during scanning my $dbh = Slim::Schema->dbh; - + # Add comments if we have them: my $sth = $dbh->prepare_cached( qq{ REPLACE INTO comments @@ -1360,8 +1354,8 @@ sub _createComments { VALUES (?, ?) } ); - - for my $comment (@{$comments}) { + + for my $comment (@{$comments}) { $sth->execute( $trackId, $comment ); main::DEBUGLOG && $log->is_debug && $log->debug("-- Track has comment '$comment'"); @@ -1371,19 +1365,19 @@ sub _createComments { sub _createTrack { my ($self, $columnValueHash, $persistentColumnValueHash, $source) = @_; - + # Create the track # Using native DBI here to improve performance during scanning my $dbh = $self->dbh; - + my $id = $self->_insertHash( tracks => $columnValueHash ); - + if ( main::INFOLOG && $log->is_info && $columnValueHash->{'title'} ) { $log->info(sprintf("Created track '%s' (id: [%d])", $columnValueHash->{'title'}, $id)); } ### Create TrackPersistent row - + if ( main::STATISTICS && $columnValueHash->{'audio'} ) { # Pull the track persistent data my $trackPersistentHash = Slim::Schema::TrackPersistent->findhash( @@ -1396,7 +1390,7 @@ sub _createTrack { $persistentColumnValueHash->{added} = time(); $persistentColumnValueHash->{url} = $columnValueHash->{url}; $persistentColumnValueHash->{urlmd5} = $columnValueHash->{urlmd5}; - + # Create a new persistent row my @pcols = keys %{$persistentColumnValueHash}; my $pcolstring = join( ',', @pcols ); @@ -1410,15 +1404,15 @@ sub _createTrack { main::INFOLOG && $log->is_info && $log->info("Updating persistent ", $columnValueHash->{url}, " : $key to $val"); $trackPersistentHash->{$key} = $val; } - + # Always update url/urlmd5 as these values may have changed if we looked up using musicbrainz_id $trackPersistentHash->{url} = $columnValueHash->{url}; $trackPersistentHash->{urlmd5} = $columnValueHash->{urlmd5}; - + $self->_updateHash( tracks_persistent => $trackPersistentHash, 'id' ); } } - + return $id; } @@ -1440,7 +1434,7 @@ Optional $args: =over 4 -=item * attributes +=item * attributes A hash ref with data to populate the object. @@ -1469,7 +1463,7 @@ Returns a new L or L object on succ sub _newTrack { my $self = shift; my $args = shift; - + my $isDebug = main::DEBUGLOG && $log->is_debug; my $isInfo = main::INFOLOG && $log->is_info; @@ -1501,7 +1495,7 @@ sub _newTrack { main::INFOLOG && $isInfo && $log->info("readTags is ". $args->{'readTags'}); $attributeHash = { %{Slim::Formats->readTags($url)}, %$attributeHash }; - + # Abort early if readTags returned nothing, meaning the file is probably bad/missing if ( !scalar keys %{$attributeHash} ) { $LAST_ERROR = 'Unable to read tags from file'; @@ -1526,9 +1520,9 @@ sub _newTrack { if ($playlist) { delete $attributeHash->{'YEAR'}; } - + ### Work out Track columns - + # Creating the track only wants lower case values from valid columns. my %columnValueHash = (); my %persistentColumnValueHash = (); @@ -1542,10 +1536,10 @@ sub _newTrack { # XXX - different check from updateOrCreate, which also checks val != '' if (defined $val && exists $trackAttrs->{$key}) { - + # Bug 7731, filter out duplicate keys that end up as array refs $val = $val->[0] if ( ref $val eq 'ARRAY' ); - + main::DEBUGLOG && $isDebug && $log->debug(" $key : $val"); $columnValueHash{$key} = $val; } @@ -1555,7 +1549,7 @@ sub _newTrack { # Bug 7731, filter out duplicate keys that end up as array refs $val = $val->[0] if ( ref $val eq 'ARRAY' ); - + main::DEBUGLOG && $isDebug && $log->debug(" (persistent) $key : $val"); $persistentColumnValueHash{$key} = $val; } @@ -1565,24 +1559,24 @@ sub _newTrack { # We don't use it anyways. $columnValueHash{'url'} = $url; $columnValueHash{'urlmd5'} = md5_hex($url); - + # Use an explicit record id if it was passed as an argument. if ($trackId) { $columnValueHash{'id'} = $trackId; } - + # Record time this track was added/updated my $now = time(); $columnValueHash{added_time} = $now; $columnValueHash{updated_time} = $now; my $ct = $columnValueHash{'content_type'}; - + # For simple (odd) cases, just create the Track row and return if (!defined $ct || $ct eq 'dir' || $ct eq 'lnk' || !$columnValueHash{'audio'}) { return $self->_createTrack(\%columnValueHash, \%persistentColumnValueHash, $source); } - + # Make a local variable for COMPILATION, that is easier to handle my $isCompilation = undef; my $compilation = $deferredAttributes->{'COMPILATION'}; @@ -1597,26 +1591,26 @@ sub _newTrack { main::DEBUGLOG && $isDebug && $log->debug("-- Track is NOT a compilation"); } } - + ### Create Contributor rows # Walk through the valid contributor roles, adding them to the database. my $contributors = $self->_mergeAndCreateContributors($deferredAttributes, $isCompilation, 1); - + # Set primary_artist for the track if ( my $artist = $contributors->{ARTIST} || $contributors->{TRACKARTIST} ) { $columnValueHash{primary_artist} = $artist->[0]; } - + ### Find artwork column values for the Track if ( !$columnValueHash{cover} && $columnValueHash{audio} ) { # Track does not have embedded artwork, look for standalone cover # findStandaloneArtwork returns either a full path to cover art or 0 # to indicate no artwork was found. my $cover = Slim::Music::Artwork->findStandaloneArtwork( \%columnValueHash, $deferredAttributes, $dirname ); - + $columnValueHash{cover} = $cover; } - + if ( $columnValueHash{cover} ) { # Generate coverid value based on artwork, mtime, filesize $columnValueHash{coverid} = Slim::Schema::Track->generateCoverId( { @@ -1628,7 +1622,7 @@ sub _newTrack { } ### Create Album row - my $albumId = $self->_createOrUpdateAlbum($deferredAttributes, + my $albumId = $self->_createOrUpdateAlbum($deferredAttributes, \%columnValueHash, # trackColumns $isCompilation, $contributors->{'ALBUMARTIST'}->[0] || $contributors->{'ARTIST'}->[0], # primary contributor-id @@ -1637,17 +1631,17 @@ sub _newTrack { undef, # Track $dirname, ); - + ### Create Track row $columnValueHash{'album'} = $albumId if !$playlist; $trackId = $self->_createTrack(\%columnValueHash, \%persistentColumnValueHash, $source); ### Create ContributorTrack & ContributorAlbum rows - $self->_createContributorRoleRelationships($contributors, $trackId, $albumId); + $self->_createContributorRoleRelationships($contributors, $trackId, $albumId); ### Create Genre rows $self->_createGenre($deferredAttributes->{'GENRE'}, $trackId, 1); - + ### Create Comment rows $self->_createComments($deferredAttributes->{'COMMENT'}, $trackId); @@ -1709,11 +1703,11 @@ sub updateOrCreate { my $args = shift; my $trackIdOrTrack = $self->updateOrCreateBase($args); - + return undef if !defined $trackIdOrTrack; - + return $trackIdOrTrack if blessed $trackIdOrTrack; - + return Slim::Schema->rs($args->{'playlist'} ? 'Playlist' : 'Track')->find($trackIdOrTrack); } @@ -1732,7 +1726,7 @@ sub updateOrCreateBase { my $isNew = $args->{'new'} || 0; # save a query if caller knows the track is new my $trackId; - + # XXX - exception should go here. Coming soon. my ($track, $url, $blessed) = _validTrackOrURL($urlOrObj); @@ -1755,7 +1749,7 @@ sub updateOrCreateBase { 'url' => $url, 'attributes' => $attributeHash, }); - + return $class->updateOrCreate($track ? $track : $url, $attributeHash); } @@ -1763,7 +1757,7 @@ sub updateOrCreateBase { if ( !defined $track && !$isNew ) { $track = $self->_retrieveTrack($url, $playlist); } - + # XXX - exception should go here. Coming soon. # _retrieveTrack will always return undef or a track object if ($track) { @@ -1782,7 +1776,7 @@ sub updateOrCreateBase { # XXX native DBI $trackPersistent = $track->retrievePersistent(); } - + # Bug: 2335 - readTags is set in Slim::Formats::Playlists::CUE - when # we create/update a cue sheet to have a CT of 'cur' if (defined $attributeHash->{'CONTENT_TYPE'} && $attributeHash->{'CONTENT_TYPE'} eq 'cur') { @@ -1803,7 +1797,7 @@ sub updateOrCreateBase { 'url' => $url, 'attributes' => $attributeHash, }); - + # Update timestamp $attributeHash->{updated_time} = time(); @@ -1837,7 +1831,7 @@ sub updateOrCreateBase { } $self->forceCommit if $commit; - + if ($track && $attributeHash->{'CONTENT_TYPE'}) { $contentTypeCache{$url} = $attributeHash->{'CONTENT_TYPE'}; } @@ -1889,7 +1883,7 @@ sub variousArtistsObject { $vaObj->namesort( Slim::Utils::Text::ignoreCaseArticles($vaString) ); $vaObj->namesearch( Slim::Utils::Text::ignoreCase($vaString, 1) ); $vaObj->update; - + # this will not change while in the external scanner $vaObjId = $vaObj->id if main::SCANNER; } @@ -1954,14 +1948,14 @@ sub totalTime { my ($self, $client) = @_; my $library_id = Slim::Music::VirtualLibraries->getLibraryIdForClient($client); - + $TOTAL_CACHE->{$library_id} ||= {}; my $totalCache = $TOTAL_CACHE->{$library_id}; if (!$totalCache->{totalTime}) { my $dbh = $self->dbh; my $sth; - + if ($library_id) { $sth = $dbh->prepare_cached('SELECT SUM(secs) FROM tracks, library_track WHERE library_track.library=? AND library_track.track=tracks.id AND tracks.audio=1'); $sth->execute($library_id); @@ -1970,11 +1964,11 @@ sub totalTime { $sth = $dbh->prepare_cached('SELECT SUM(secs) FROM tracks WHERE tracks.audio=1'); $sth->execute(); } - + ($totalCache->{totalTime}) = $sth->fetchrow_array; $sth->finish; } - + return $totalCache->{totalTime}; } @@ -1986,13 +1980,13 @@ Merge a single VA album sub mergeSingleVAAlbum { my ( $class, $albumid, $returnIsComp ) = @_; - + my $importlog = main::INFOLOG ? logger('scan.import') : undef; my $isInfo = main::INFOLOG && $importlog->is_info; - + my $dbh = $class->dbh; my ($is_comp, $is_comp_db); - + # if album already is flagged as a compilation, we don't need to continue the evaluation if ($returnIsComp) { my $iscomp_sth = $dbh->prepare_cached( qq{ @@ -2000,16 +1994,16 @@ sub mergeSingleVAAlbum { FROM albums WHERE id = ? } ); - + $iscomp_sth->execute($albumid); ($is_comp_db) = $iscomp_sth->fetchrow_array; $iscomp_sth->finish; - + return 1 if $is_comp_db; } - + my $role = Slim::Schema::Contributor->typeToRole('ARTIST'); - + my $track_contribs_sth = $dbh->prepare_cached( qq{ SELECT contributor, track FROM contributor_track @@ -2017,22 +2011,22 @@ sub mergeSingleVAAlbum { SELECT id FROM tracks WHERE album = ? - ) + ) AND role = ? ORDER BY contributor, track } ); - + # Check track contributors to see if all tracks have the same contributors my ($contributor, $trackid); my %track_contribs; - + $track_contribs_sth->execute( $albumid, $role ); $track_contribs_sth->bind_columns( \$contributor, \$trackid ); - + while ( $track_contribs_sth->fetch ) { $track_contribs{ $contributor } .= $trackid . ':'; } - + my $track_list; for my $tracks ( values %track_contribs ) { if ( $track_list && $track_list ne $tracks ) { @@ -2042,19 +2036,19 @@ sub mergeSingleVAAlbum { } $track_list = $tracks; } - + if ( $returnIsComp ) { # Optimization used to avoid extra query when updating an album entry return $is_comp; } - + if ( $is_comp ) { my $comp_sth = $dbh->prepare_cached( qq{ UPDATE albums SET compilation = 1, contributor = ? WHERE id = ? } ); - + # Flag as a compilation, set primary contrib to Various Artists $comp_sth->execute( $vaObjId || $class->variousArtistsObject->id, $albumid ); } @@ -2065,7 +2059,7 @@ sub mergeSingleVAAlbum { SET compilation = 0 WHERE id = ? } ); - + # Cache that the album is not a compilation so it's not constantly # checked during every mergeVA phase. Scanner::Local will reset # compilation to undef when a new/deleted/changed track requires @@ -2086,7 +2080,7 @@ sub wipeCaches { $self->forceCommit; %contentTypeCache = (); - + $TOTAL_CACHE = {}; # clear the references to these singletons @@ -2098,7 +2092,7 @@ sub wipeCaches { $self->lastTrackURL(''); $self->lastTrack({}); $lastAlbum = {}; - + main::INFOLOG && logger('scan.import')->info("Wiped all in-memory caches."); } @@ -2110,7 +2104,7 @@ Wipe the lastAlbum cache, if it contains the album $id sub wipeLastAlbumCache { my ( $self, $id ) = @_; - + if ( defined $id && exists $lastAlbum->{id} && $lastAlbum->{id} == $id ) { $lastAlbum = {}; } @@ -2128,7 +2122,7 @@ sub wipeAllData { $self->schemaUpdated(undef); $self->wipeCaches; $self->wipeDB; - + require Slim::Utils::ArtworkCache; Slim::Utils::ArtworkCache->new()->wipe(); @@ -2237,7 +2231,7 @@ sub rating { my ( $class, $track, $rating ) = @_; my $impl = $prefs->get('ratingImplementation'); - + if ( !$impl || !exists $ratingImplementations{$impl} ) { $impl = 'LOCAL_RATING_STORAGE'; } @@ -2257,7 +2251,7 @@ sub _defaultRatingImplementation { $track->update; Slim::Schema->forceCommit; } - + return $track->rating; } @@ -2268,7 +2262,7 @@ sub _retrieveTrack { return undef if ref($url); my $track; - + if (Slim::Music::Info::isRemoteURL($url)) { return Slim::Schema::RemoteTrack->fetch($url, $playlist); } @@ -2292,8 +2286,8 @@ sub _retrieveTrack { if (!$playlist || $track->audio) { $self->lastTrackURL($url); $self->lastTrack->{$dirname} = $track; - - # Set the contentTypeCache entry here is case + + # Set the contentTypeCache entry here is case # it was guessed earlier without knowing the real type $contentTypeCache{$url} = $track->content_type; } @@ -2331,11 +2325,11 @@ sub _checkValidity { # XXX - exception should go here. Coming soon. return undef unless blessed($track); return undef unless $track->can('get'); - + # Remote tracks are always assumed to be valid # Maybe we will add a timeout mechanism later return $track if $track->isRemoteURL(); - + my $isDebug = main::DEBUGLOG && $log->is_debug; my $url = $track->get('url'); @@ -2346,11 +2340,11 @@ sub _checkValidity { main::DEBUGLOG && $isDebug && $log->debug("Re-reading tags from $url as it has changed."); my $oldid = $track->id; - + # Do a cascading delete for has_many relationships - this will # clear out Contributors, Genres, etc. $track->delete; - + # Add the track back into database with the same id as the record deleted. my $trackId = $self->_newTrack({ 'id' => $oldid, @@ -2358,10 +2352,10 @@ sub _checkValidity { 'readTags' => 1, 'commit' => 1, }); - + $track = Slim::Schema->rs('Track')->find($trackId) if (defined $trackId); } - + # Track may have been deleted by _hasChanged return undef unless $track->in_storage; @@ -2373,7 +2367,7 @@ sub _checkValidity { sub _hasChanged { my ($self, $track, $url) = @_; - + my $isDebug = main::DEBUGLOG && $log->is_debug; # We return 0 if the file hasn't changed @@ -2386,7 +2380,7 @@ sub _hasChanged { # main::DEBUGLOG && $isDebug && $log->debug("Checking for [$filepath] - size & timestamp."); - # Return if it's a directory - they expire themselves + # Return if it's a directory - they expire themselves # Todo - move directory expire code here? return 0 if -d $filepath; return 0 if $filepath =~ /\.lnk$/i; @@ -2422,11 +2416,11 @@ sub _hasChanged { return 1; } else { - + # Bug 4402, if the entire volume/drive this file is on is unavailable, # it's likely removable storage and shouldn't be deleted my $offline; - + if ( main::ISWINDOWS ) { # win32, check the drive letter my $parent = Path::Class::File->new($filepath)->dir; @@ -2448,7 +2442,7 @@ sub _hasChanged { # XXX: Linux/Unix, not sure how to tell if a given path # is from an unmounted filesystem } - + if ( $offline ) { main::DEBUGLOG && $isDebug && $log->debug( "Drive/Volume containing [$filepath] seems to be offline, skipping" ); return 0; @@ -2500,7 +2494,7 @@ sub _preCheckAttributes { $attributes->{ $key } = $val; } } - + # Bug 9359, don't allow tags named 'ID' if ( exists $attributes->{'ID'} ) { delete $attributes->{'ID'}; @@ -2519,7 +2513,7 @@ sub _preCheckAttributes { if ($attributes->{'TITLE'}) { # Create a canonical title to search against. $attributes->{'TITLESEARCH'} = Slim::Utils::Text::ignoreCase($attributes->{'TITLE'}, 1); - + if (!$attributes->{'TITLESORT'}) { $attributes->{'TITLESORT'} = Slim::Utils::Text::ignoreCaseArticles($attributes->{'TITLE'}); } else { @@ -2538,7 +2532,7 @@ sub _preCheckAttributes { } # Some tag formats - APE? store the type of channels instead of the number of channels. - if (defined $attributes->{'CHANNELS'}) { + if (defined $attributes->{'CHANNELS'}) { if ($attributes->{'CHANNELS'} =~ /stereo/i) { $attributes->{'CHANNELS'} = 2; } elsif ($attributes->{'CHANNELS'} =~ /mono/i) { @@ -2550,10 +2544,10 @@ sub _preCheckAttributes { # Same for DISC - Bug 2821 for my $tag (qw(YEAR DISC DISCC BPM CHANNELS)) { - if ( - defined $attributes->{$tag} + if ( + defined $attributes->{$tag} && - ( $attributes->{$tag} !~ /^\d+$/ || $attributes->{$tag} == 0 ) + ( $attributes->{$tag} !~ /^\d+$/ || $attributes->{$tag} == 0 ) ) { delete $attributes->{$tag}; } @@ -2572,7 +2566,7 @@ sub _preCheckAttributes { for my $tag (qw(YEAR RATING)) { $attributes->{$tag} ||= 0; } - + # Bug 4803, ensure rating is an integer that fits into tinyint if ( $attributes->{RATING} && ($attributes->{RATING} !~ /^\d+$/ || $attributes->{RATING} > 255) ) { logWarning("Invalid RATING tag '" . $attributes->{RATING} . "' in " . Slim::Utils::Misc::pathFromFileURL($url)); @@ -2590,17 +2584,17 @@ sub _preCheckAttributes { $shortTag =~ s/^REPLAYGAIN_TRACK_(\w+)$/REPLAY_$1/; if (defined $attributes->{$gainTag}) { - + $attributes->{$shortTag} = delete $attributes->{$gainTag}; $attributes->{$shortTag} =~ s/\s*dB//gi; $attributes->{$shortTag} =~ s/\s//g; # bug 15965 $attributes->{$shortTag} =~ s/,/\./g; # bug 6900, change comma to period - + # Bug 15483, remove non-numeric gain tags if ( $attributes->{$shortTag} !~ /^[\d\-\+\.]+$/ ) { my $file = Slim::Utils::Misc::pathFromFileURL($url); $log->error("Invalid ReplayGain tag found in $file: $gainTag -> " . $attributes->{$shortTag} ); - + delete $attributes->{$shortTag}; } } @@ -2629,7 +2623,7 @@ sub _preCheckAttributes { # Look for tags we don't want to expose in comments, and splice them out. for my $c ( @{$rawcomments} ) { next unless defined $c; - + # Bug 15630, ignore strings which have the utf8 flag on but are in fact invalid utf8 # XXX - I can no longer reproduce the issues reported in 15630, but it's causing bug 17863 -michael #next if utf8::is_utf8($c) && !Slim::Utils::Unicode::looks_like_utf8($c); @@ -2643,7 +2637,7 @@ sub _preCheckAttributes { next; } - + push @$comments, $c; } @@ -2661,8 +2655,8 @@ sub _preCheckAttributes { # Push these back until we have a Track object. for my $tag (Slim::Schema::Contributor->contributorRoles, qw( COMMENT GENRE ARTISTSORT PIC APIC ALBUM ALBUMSORT DISCC - COMPILATION REPLAYGAIN_ALBUM_PEAK REPLAYGAIN_ALBUM_GAIN - MUSICBRAINZ_ARTIST_ID MUSICBRAINZ_ALBUMARTIST_ID MUSICBRAINZ_ALBUM_ID + COMPILATION REPLAYGAIN_ALBUM_PEAK REPLAYGAIN_ALBUM_GAIN + MUSICBRAINZ_ARTIST_ID MUSICBRAINZ_ALBUMARTIST_ID MUSICBRAINZ_ALBUM_ID MUSICBRAINZ_ALBUM_TYPE MUSICBRAINZ_ALBUM_STATUS ALBUMARTISTSORT COMPOSERSORT CONDUCTORSORT BANDSORT )) { @@ -2671,7 +2665,7 @@ sub _preCheckAttributes { $deferredAttributes->{$tag} = delete $attributes->{$tag}; } - + # If embedded artwork was found, store the length of the artwork if ( $attributes->{'COVER_LENGTH'} ) { $attributes->{'COVER'} = delete $attributes->{'COVER_LENGTH'}; @@ -2682,7 +2676,7 @@ sub _preCheckAttributes { # thumb has gone away, since we have GD resizing. delete $attributes->{'THUMB'}; - + # RemoteTrack also wants artist and album names if ($attributes->{'REMOTE'}) { foreach (qw/TRACKARTIST ARTIST ALBUMARTIST/) { @@ -2693,7 +2687,7 @@ sub _preCheckAttributes { } } $attributes->{'ALBUMNAME'} = $deferredAttributes->{'ALBUM'} if $deferredAttributes->{'ALBUM'}; - + # XXX maybe also want COMMENT & GENRE } @@ -2724,17 +2718,17 @@ sub _preCheckAttributes { sub _createGenre { my ($self, $genre, $trackId, $create) = @_; - + # Genre addition. If there's no genre for this track, and no 'No Genre' object, create one. my $isDebug = main::DEBUGLOG && $log->is_debug; - + if ($genre) { # Bug 17322, strip leading/trailing spaces from name $genre =~ s/^ +//; $genre =~ s/ +$//; } - + if ($create && !$genre && !blessed($_unknownGenre)) { my $genreName = string('NO_GENRE'); @@ -2778,19 +2772,19 @@ sub _createGenre { SELECT genres.name FROM genre_track JOIN genres ON genres.id = genre_track.genre WHERE genre_track.track = ? } ); $sth->execute($trackId); - + # compare the list of ordered, lower case genre names, new and old my $newGenres = join('::', sort map { lc($_->[0]) } @{ $sth->fetchall_arrayref() || [] }); my $oldGenres = join('::', sort map { lc($_) } Slim::Music::Info::splitTag($genre)); - + if ($newGenres ne $oldGenres) { # Bug 1143: The user has updated the genre tag, and is # rescanning We need to remove the previous associations. my $track = Slim::Schema->rs('Track')->find($trackId); $track->genreTracks->delete_all; - + Slim::Schema::Genre->add($genre, $trackId); - + main::DEBUGLOG && $isDebug && $log->debug("-- Deleted all previous genres for this track"); main::DEBUGLOG && $isDebug && $log->debug("-- Track has genre '$genre'"); } @@ -2800,7 +2794,7 @@ sub _createGenre { sub _postCheckAttributes { my $self = shift; my $args = shift; - + my $isDebug = main::DEBUGLOG && $log->is_debug; my $track = $args->{'track'}; @@ -2811,14 +2805,14 @@ sub _postCheckAttributes { # etc don't show up if you don't have any. my %cols = $track->get_columns; - my ($trackId, $trackUrl, $trackType, $trackAudio, $trackRemote) = + my ($trackId, $trackUrl, $trackType, $trackAudio, $trackRemote) = (@cols{qw/id url content_type audio remote/}); if (!defined $trackType || $trackType eq 'dir' || $trackType eq 'lnk') { $track->update; return undef; } - + if ($trackRemote || !$trackAudio) { $track->update; return; @@ -2839,12 +2833,12 @@ sub _postCheckAttributes { } $self->_createGenre($attributes->{'GENRE'}, $trackId, $create); - + # Walk through the valid contributor roles, adding them to the database. my $contributors = $self->_mergeAndCreateContributors($attributes, $isCompilation, $create); ### Update Album row - my $albumId = $self->_createOrUpdateAlbum($attributes, + my $albumId = $self->_createOrUpdateAlbum($attributes, \%cols, # trackColumns $isCompilation, $contributors->{'ALBUMARTIST'}->[0] || $contributors->{'ARTIST'}->[0], # primary contributor-id @@ -2852,19 +2846,19 @@ sub _postCheckAttributes { $create, # create $track, # Track ); - + # Don't add an album to container tracks - See bug 2337 if (!Slim::Music::Info::isContainer($track, $trackType)) { $track->album($albumId); } - $self->_createContributorRoleRelationships($contributors, $trackId, $albumId); + $self->_createContributorRoleRelationships($contributors, $trackId, $albumId); # Save any changes - such as album. $track->update; - + $self->_createComments($attributes->{'COMMENT'}, $trackId); - + # refcount-- %{$contributors} = (); } @@ -2894,13 +2888,13 @@ sub _mergeAndCreateContributors { )); } } - + my %contributors = (); for my $tag (Slim::Schema::Contributor->contributorRoles) { my $contributor = $attributes->{$tag} || next; - + # Bug 17322, strip leading/trailing spaces from name $contributor =~ s/^ +//; $contributor =~ s/ +$//; @@ -2909,19 +2903,19 @@ sub _mergeAndCreateContributors { # contributors? I think so. ID3 doesn't have # "BANDSORT" or similar at any rate. push @{ $contributors{$tag} }, Slim::Schema::Contributor->add({ - 'artist' => $contributor, + 'artist' => $contributor, 'brainzID' => $attributes->{"MUSICBRAINZ_${tag}_ID"}, 'sortBy' => $attributes->{$tag.'SORT'}, }); main::DEBUGLOG && $isDebug && $log->is_debug && $log->debug(sprintf("-- Track has contributor '$contributor' of role '$tag'")); } - + # Bug 15553, Primary contributor can only be Album Artist or Artist, # so only check for those roles and assign No Artist otherwise my $foundContributor = ($contributors{'ALBUMARTIST'} && $contributors{'ALBUMARTIST'}->[0] || $contributors{'ARTIST'} && $contributors{'ARTIST'}->[0]); - + main::DEBUGLOG && $isDebug && $log->debug("-- Track has ", scalar (keys %contributors), " contributor(s)"); # Create a singleton for "No Artist" @@ -2946,45 +2940,45 @@ sub _mergeAndCreateContributors { main::DEBUGLOG && $isDebug && $log->debug("-- Track has no artist"); } - + return \%contributors; } sub _createContributorRoleRelationships { - + my ($self, $contributors, $trackId, $albumId) = @_; - + if (!keys %$contributors) { main::DEBUGLOG && $log->debug('Attempt to set empty contributor set for trackid=', $trackId); return; } - + # Wipe track contributors for this track, this is necessary to handle # a changed track where contributors have been removed. Current contributors # will be re-added by below my $sth_delete_tracks = $self->dbh->prepare_cached( qq{ - DELETE - FROM contributor_track + DELETE + FROM contributor_track WHERE track = ? } ); $sth_delete_tracks->execute($trackId); # Using native DBI here to improve performance during scanning - + my $sth_track = $self->dbh->prepare_cached( qq{ REPLACE INTO contributor_track (role, contributor, track) VALUES (?, ?, ?) } ); - + my $sth_album = $self->dbh->prepare_cached( qq{ REPLACE INTO contributor_album (role, contributor, album) VALUES (?, ?, ?) } ); - + while (my ($role, $contributorList) = each %{$contributors}) { my $roleId = Slim::Schema::Contributor->typeToRole($role); for my $contributor (@{$contributorList}) { @@ -3005,7 +2999,7 @@ my %lastTrackOrUrl = ( sub _validTrackOrURL { my $urlOrObj = shift; - + if ($lastTrackOrUrl{obj} eq $urlOrObj) { return ($lastTrackOrUrl{track}, $lastTrackOrUrl{url}, $lastTrackOrUrl{blessed}); } @@ -3029,7 +3023,7 @@ sub _validTrackOrURL { $url = $urlOrObj; } } - + %lastTrackOrUrl = ( obj => $urlOrObj, track => $track, @@ -3042,7 +3036,7 @@ sub _validTrackOrURL { sub isaTrack { my $obj = shift; - + return $obj && blessed $obj && ($obj->isa('Slim::Schema::Track') || $obj->isa('Slim::Schema::RemoteTrack')); } @@ -3054,12 +3048,12 @@ sub lastError { $LAST_ERROR } sub totals { my ($class, $client) = @_; - + my $library_id = Slim::Music::VirtualLibraries->getLibraryIdForClient($client); - + $TOTAL_CACHE->{$library_id} ||= {}; my $totalCache = $TOTAL_CACHE->{$library_id}; - + my %categories = ( album => ['albums', 0, 1, 'tags:CC'], contributor => ['artists', 0, 1, 'tags:CC'], @@ -3067,7 +3061,7 @@ sub totals { track => ['titles', 0, 1, 'tags:CC'], playlist => ['playlists', 0, 1, 'tags:CC'], ); - + while (my ($key, $query) = each %categories) { if ( !$totalCache->{$key} ) { push @$query, 'library_id:' . $library_id if $library_id; @@ -3077,48 +3071,48 @@ sub totals { main::idleStreams(); } } - + return $totalCache; } sub _insertHash { my ( $class, $table, $hash ) = @_; - + my $dbh = $class->dbh; - + my @cols = keys %{$hash}; my $colstring = join( ',', @cols ); my $ph = join( ',', map { '?' } @cols ); - + my $sth = $dbh->prepare("INSERT INTO $table ($colstring) VALUES ($ph)"); $sth->execute( map { $hash->{$_} } @cols ); - + return $dbh->last_insert_id(undef, undef, undef, undef); } sub _updateHash { my ( $class, $table, $hash, $pk ) = @_; - + my $id = delete $hash->{$pk}; - + # Construct SQL with placeholders for non-null values and NULL for null values my @cols = keys %{$hash}; my $colstring = join( ', ', map { $_ . (defined $hash->{$_} ? ' = ?' : ' = NULL') } @cols ); - + my $sth = $class->dbh->prepare("UPDATE $table SET $colstring WHERE $pk = ?"); $sth->execute( (grep { defined $_ } map { $hash->{$_} } @cols), $id ); - + $hash->{$pk} = $id; - + return 1; } my $canFulltextSearch; sub canFulltextSearch { return $canFulltextSearch if defined $canFulltextSearch; - + $canFulltextSearch = Slim::Utils::PluginManager->isEnabled('Slim::Plugin::FullTextSearch::Plugin') && Slim::Plugin::FullTextSearch::Plugin->canFulltextSearch; - return $canFulltextSearch; + return $canFulltextSearch; } =head1 SEE ALSO diff --git a/Slim/Schema/Album.pm b/Slim/Schema/Album.pm index 6b797553788..2c42eca9fdd 100644 --- a/Slim/Schema/Album.pm +++ b/Slim/Schema/Album.pm @@ -1,6 +1,5 @@ package Slim::Schema::Album; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/Comment.pm b/Slim/Schema/Comment.pm index ac47b1a1d44..a92798d6143 100644 --- a/Slim/Schema/Comment.pm +++ b/Slim/Schema/Comment.pm @@ -1,6 +1,5 @@ package Slim::Schema::Comment; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/Contributor.pm b/Slim/Schema/Contributor.pm index 5b290323b1c..54c04a0a516 100644 --- a/Slim/Schema/Contributor.pm +++ b/Slim/Schema/Contributor.pm @@ -1,6 +1,5 @@ package Slim::Schema::Contributor; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/ContributorAlbum.pm b/Slim/Schema/ContributorAlbum.pm index ab2c83bfe23..24870953bfe 100644 --- a/Slim/Schema/ContributorAlbum.pm +++ b/Slim/Schema/ContributorAlbum.pm @@ -1,6 +1,5 @@ package Slim::Schema::ContributorAlbum; -# $Id$ # # Contributor to album mapping class diff --git a/Slim/Schema/ContributorTrack.pm b/Slim/Schema/ContributorTrack.pm index 499c15390be..ca8288d05ba 100644 --- a/Slim/Schema/ContributorTrack.pm +++ b/Slim/Schema/ContributorTrack.pm @@ -1,6 +1,5 @@ package Slim::Schema::ContributorTrack; -# $Id$ # # Contributor to track mapping class diff --git a/Slim/Schema/DBI.pm b/Slim/Schema/DBI.pm index 0c3395f1d80..68db0312864 100644 --- a/Slim/Schema/DBI.pm +++ b/Slim/Schema/DBI.pm @@ -1,8 +1,7 @@ package Slim::Schema::DBI; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Schema/Debug.pm b/Slim/Schema/Debug.pm index 1488abe71ce..87a3c57d03d 100644 --- a/Slim/Schema/Debug.pm +++ b/Slim/Schema/Debug.pm @@ -1,8 +1,7 @@ package Slim::Schema::Debug; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Schema/Genre.pm b/Slim/Schema/Genre.pm index bbf8b027ea7..a909e9e4ac9 100644 --- a/Slim/Schema/Genre.pm +++ b/Slim/Schema/Genre.pm @@ -1,6 +1,5 @@ package Slim::Schema::Genre; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/GenreTrack.pm b/Slim/Schema/GenreTrack.pm index c954c300767..2db50e9bac9 100644 --- a/Slim/Schema/GenreTrack.pm +++ b/Slim/Schema/GenreTrack.pm @@ -1,6 +1,5 @@ package Slim::Schema::GenreTrack; -# $Id$ # # Genre to track mapping class diff --git a/Slim/Schema/MetaInformation.pm b/Slim/Schema/MetaInformation.pm index d6d4d091311..03cd5c70943 100644 --- a/Slim/Schema/MetaInformation.pm +++ b/Slim/Schema/MetaInformation.pm @@ -1,6 +1,5 @@ package Slim::Schema::MetaInformation; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/Playlist.pm b/Slim/Schema/Playlist.pm index 3be3b7b3685..6a597a7a92c 100644 --- a/Slim/Schema/Playlist.pm +++ b/Slim/Schema/Playlist.pm @@ -1,6 +1,5 @@ package Slim::Schema::Playlist; -# $Id$ use strict; use base 'Slim::Schema::Track'; diff --git a/Slim/Schema/PlaylistTrack.pm b/Slim/Schema/PlaylistTrack.pm index 814012def37..f0f6d58311e 100644 --- a/Slim/Schema/PlaylistTrack.pm +++ b/Slim/Schema/PlaylistTrack.pm @@ -1,6 +1,5 @@ package Slim::Schema::PlaylistTrack; -# $Id$ # # Playlist to track mapping class diff --git a/Slim/Schema/Progress.pm b/Slim/Schema/Progress.pm index c6e5c288af0..9b64b1c86a4 100644 --- a/Slim/Schema/Progress.pm +++ b/Slim/Schema/Progress.pm @@ -1,6 +1,5 @@ package Slim::Schema::Progress; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/RemotePlaylist.pm b/Slim/Schema/RemotePlaylist.pm index 07d47c2d1c0..60dcf903f1c 100644 --- a/Slim/Schema/RemotePlaylist.pm +++ b/Slim/Schema/RemotePlaylist.pm @@ -1,6 +1,5 @@ package Slim::Schema::RemotePlaylist; -# $Id$ # This is an emulation of the Slim::Schema::Playlist API for remote tracks diff --git a/Slim/Schema/RemoteTrack.pm b/Slim/Schema/RemoteTrack.pm index a63ac9b5ed3..b46c0bfb022 100644 --- a/Slim/Schema/RemoteTrack.pm +++ b/Slim/Schema/RemoteTrack.pm @@ -1,5 +1,10 @@ package Slim::Schema::RemoteTrack; +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + # This is an emulation of the Slim::Schema::Track API for remote tracks use strict; @@ -389,8 +394,9 @@ sub fetchById { sub get { my ($self, $attribute) = @_; - main::DEBUGLOG && $log->is_debug && $log->debug($self->_url, ', ', $attribute, '->', $self->$attribute()); - + main::DEBUGLOG && $log->is_debug && + $log->debug($self->_url, ', ', $attribute, '->', $self->$attribute() || "* undefined value *"); + return($self->$attribute()); } diff --git a/Slim/Schema/Rescan.pm b/Slim/Schema/Rescan.pm index b74b036f5c5..2551e8978fe 100644 --- a/Slim/Schema/Rescan.pm +++ b/Slim/Schema/Rescan.pm @@ -1,6 +1,5 @@ package Slim::Schema::Rescan; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/ResultSet/Album.pm b/Slim/Schema/ResultSet/Album.pm index 05192b4d3fb..df1fe04fa75 100644 --- a/Slim/Schema/ResultSet/Album.pm +++ b/Slim/Schema/ResultSet/Album.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::Album; -# $Id$ use strict; use base qw(Slim::Schema::ResultSet::Base); diff --git a/Slim/Schema/ResultSet/Base.pm b/Slim/Schema/ResultSet/Base.pm index 1f16a24220d..98a41faf6b5 100644 --- a/Slim/Schema/ResultSet/Base.pm +++ b/Slim/Schema/ResultSet/Base.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::Base; -# $Id$ # Base class for ResultSets - override what you need. diff --git a/Slim/Schema/ResultSet/Contributor.pm b/Slim/Schema/ResultSet/Contributor.pm index f9a26a7a04c..a718756dc4b 100644 --- a/Slim/Schema/ResultSet/Contributor.pm +++ b/Slim/Schema/ResultSet/Contributor.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::Contributor; -# $Id$ use strict; use base qw(Slim::Schema::ResultSet::Base); diff --git a/Slim/Schema/ResultSet/Genre.pm b/Slim/Schema/ResultSet/Genre.pm index edd62ab9f31..1a5d72a7368 100644 --- a/Slim/Schema/ResultSet/Genre.pm +++ b/Slim/Schema/ResultSet/Genre.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::Genre; -# $Id$ use strict; use base qw(Slim::Schema::ResultSet::Base); diff --git a/Slim/Schema/ResultSet/Playlist.pm b/Slim/Schema/ResultSet/Playlist.pm index cced48abe34..20c4ab18534 100644 --- a/Slim/Schema/ResultSet/Playlist.pm +++ b/Slim/Schema/ResultSet/Playlist.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::Playlist; -# $Id$ use strict; use base qw(Slim::Schema::ResultSet::Base); diff --git a/Slim/Schema/ResultSet/PlaylistTrack.pm b/Slim/Schema/ResultSet/PlaylistTrack.pm index cfae57c41ad..38c336fb932 100644 --- a/Slim/Schema/ResultSet/PlaylistTrack.pm +++ b/Slim/Schema/ResultSet/PlaylistTrack.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::PlaylistTrack; -# $Id$ use strict; use base qw(Slim::Schema::ResultSet::Track); diff --git a/Slim/Schema/ResultSet/Track.pm b/Slim/Schema/ResultSet/Track.pm index b63c246117f..b432d49631c 100644 --- a/Slim/Schema/ResultSet/Track.pm +++ b/Slim/Schema/ResultSet/Track.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::Track; -# $Id$ use strict; use base qw(Slim::Schema::ResultSet::Base); diff --git a/Slim/Schema/ResultSet/Year.pm b/Slim/Schema/ResultSet/Year.pm index c3f003c17d6..b83550d84fb 100644 --- a/Slim/Schema/ResultSet/Year.pm +++ b/Slim/Schema/ResultSet/Year.pm @@ -1,6 +1,5 @@ package Slim::Schema::ResultSet::Year; -# $Id$ use strict; use base qw(Slim::Schema::ResultSet::Base); diff --git a/Slim/Schema/Storage.pm b/Slim/Schema/Storage.pm index 13b050c52da..e1866a89cda 100644 --- a/Slim/Schema/Storage.pm +++ b/Slim/Schema/Storage.pm @@ -1,8 +1,7 @@ package Slim::Schema::Storage; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Schema/Track.pm b/Slim/Schema/Track.pm index 9bba4fc315a..0ffe3f2f694 100644 --- a/Slim/Schema/Track.pm +++ b/Slim/Schema/Track.pm @@ -1,6 +1,5 @@ package Slim::Schema::Track; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/TrackPersistent.pm b/Slim/Schema/TrackPersistent.pm index b81f8e57280..5c2a407a836 100644 --- a/Slim/Schema/TrackPersistent.pm +++ b/Slim/Schema/TrackPersistent.pm @@ -1,6 +1,5 @@ package Slim::Schema::TrackPersistent; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Schema/Year.pm b/Slim/Schema/Year.pm index 6750ee6f833..7250eb798c1 100644 --- a/Slim/Schema/Year.pm +++ b/Slim/Schema/Year.pm @@ -1,6 +1,5 @@ package Slim::Schema::Year; -# $Id$ use strict; use base 'Slim::Schema::DBI'; diff --git a/Slim/Utils/Accessor.pm b/Slim/Utils/Accessor.pm index 949e7c4efce..a442e8232c7 100644 --- a/Slim/Utils/Accessor.pm +++ b/Slim/Utils/Accessor.pm @@ -1,8 +1,7 @@ package Slim::Utils::Accessor; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/Alarm.pm b/Slim/Utils/Alarm.pm index 77f093d6bcd..a9eb79cb61d 100644 --- a/Slim/Utils/Alarm.pm +++ b/Slim/Utils/Alarm.pm @@ -4,7 +4,7 @@ use strict; # Max Spicer, May 2008 # This code is derived from code with the following copyright message: # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/ArtworkCache.pm b/Slim/Utils/ArtworkCache.pm index e91b295a35b..87dd1c49714 100644 --- a/Slim/Utils/ArtworkCache.pm +++ b/Slim/Utils/ArtworkCache.pm @@ -68,6 +68,10 @@ sub set { my $ref = $data->{data_ref}; + $data->{content_type} ||= ''; + $data->{mtime} ||= 0; + $data->{original_path} ||= ''; + my $packed = pack( 'A3LS', $data->{content_type}, $data->{mtime}, length( $data->{original_path} ) ) . $data->{original_path}; diff --git a/Slim/Utils/AutoRescan.pm b/Slim/Utils/AutoRescan.pm index bae99418cce..f704349084d 100644 --- a/Slim/Utils/AutoRescan.pm +++ b/Slim/Utils/AutoRescan.pm @@ -1,11 +1,10 @@ package Slim::Utils::AutoRescan; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ # This class handles file system change detection and auto-rescan on # Mac 10.5+, Linux with inotify, and Windows. diff --git a/Slim/Utils/AutoRescan/Linux.pm b/Slim/Utils/AutoRescan/Linux.pm index 0ea2510e48a..feeab30bd43 100644 --- a/Slim/Utils/AutoRescan/Linux.pm +++ b/Slim/Utils/AutoRescan/Linux.pm @@ -1,11 +1,10 @@ package Slim::Utils::AutoRescan::Linux; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ use strict; diff --git a/Slim/Utils/AutoRescan/OSX.pm b/Slim/Utils/AutoRescan/OSX.pm index 1b6551e3409..e72def897b8 100644 --- a/Slim/Utils/AutoRescan/OSX.pm +++ b/Slim/Utils/AutoRescan/OSX.pm @@ -1,11 +1,10 @@ package Slim::Utils::AutoRescan::OSX; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ use strict; diff --git a/Slim/Utils/AutoRescan/Stat.pm b/Slim/Utils/AutoRescan/Stat.pm index 874b9290d88..9d6d6433d99 100644 --- a/Slim/Utils/AutoRescan/Stat.pm +++ b/Slim/Utils/AutoRescan/Stat.pm @@ -1,11 +1,10 @@ package Slim::Utils::AutoRescan::Stat; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ use strict; diff --git a/Slim/Utils/AutoRescan/Stat/AIO.pm b/Slim/Utils/AutoRescan/Stat/AIO.pm index b7fd10d667a..441413aaefc 100644 --- a/Slim/Utils/AutoRescan/Stat/AIO.pm +++ b/Slim/Utils/AutoRescan/Stat/AIO.pm @@ -1,8 +1,7 @@ package Slim::Utils::AutoRescan::Stat::AIO; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. diff --git a/Slim/Utils/AutoRescan/Stat/Async.pm b/Slim/Utils/AutoRescan/Stat/Async.pm index 0144396e7b2..4345ff82b42 100644 --- a/Slim/Utils/AutoRescan/Stat/Async.pm +++ b/Slim/Utils/AutoRescan/Stat/Async.pm @@ -1,8 +1,7 @@ package Slim::Utils::AutoRescan::Stat::Async; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. diff --git a/Slim/Utils/AutoRescan/Win32.pm b/Slim/Utils/AutoRescan/Win32.pm index eca6bb55dfd..807691d3460 100644 --- a/Slim/Utils/AutoRescan/Win32.pm +++ b/Slim/Utils/AutoRescan/Win32.pm @@ -1,11 +1,10 @@ package Slim::Utils::AutoRescan::Win32; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ use strict; diff --git a/Slim/Utils/Cache.pm b/Slim/Utils/Cache.pm index b23cfc210a5..fae6c1936a5 100644 --- a/Slim/Utils/Cache.pm +++ b/Slim/Utils/Cache.pm @@ -1,4 +1,4 @@ -# Copyright 2005-2009 Logitech +# Logitech Media Server Copyright 2005-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Utils/DateTime.pm b/Slim/Utils/DateTime.pm index c9ccbb231d8..fa824976b59 100644 --- a/Slim/Utils/DateTime.pm +++ b/Slim/Utils/DateTime.pm @@ -1,6 +1,6 @@ package Slim::Utils::DateTime; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/DbCache.pm b/Slim/Utils/DbCache.pm index 0d17b73b412..16ba4cb0cb2 100644 --- a/Slim/Utils/DbCache.pm +++ b/Slim/Utils/DbCache.pm @@ -22,7 +22,7 @@ sub new { if ( !defined $args->{root} ) { require Slim::Utils::Prefs; $args->{root} = Slim::Utils::Prefs::preferences('server')->get('cachedir'); - + # Update root value if librarycachedir changes Slim::Utils::Prefs::preferences('server')->setChange( sub { $self->wipe; @@ -30,9 +30,9 @@ sub new { $self->_init_db; }, 'cachedir' ); } - + $args->{default_expires_in} ||= DEFAULT_EXPIRES_TIME; - + return bless $args, $self; } @@ -42,13 +42,13 @@ sub getRoot { sub setRoot { my ( $self, $root ) = @_; - + $self->{root} = $root; } sub wipe { my $self = shift; - + if ( my $dbh = $self->_init_db ) { $dbh->do('DELETE FROM cache'); # truncate $self->_close_db; @@ -58,17 +58,17 @@ sub wipe { sub set { my ( $self, $key, $data, $expiry ) = @_; - + $self->_init_db; - + $expiry = _canonicalize_expiration_time(defined $expiry ? $expiry : $self->{default_expires_in}); my $id = _key($key); - + if (ref $data) { $data = freeze( $data ); } - + # Insert or replace the value my $set = $self->{set_sth}; $set->bind_param( 1, $id ); @@ -79,7 +79,7 @@ sub set { sub get { my ( $self, $key ) = @_; - + $self->_init_db; my $id = _key($key); @@ -88,16 +88,16 @@ sub get { $get->execute($id); my ($data, $expiry) = $get->fetchrow_array; - + if ($expiry && !$self->{noexpiry} && $expiry >= 0 && $expiry < time()) { $data = undef; # $self->{delete_sth}->execute($id); } - + eval { $data = thaw($data); } if $data; - + return $data; } @@ -105,22 +105,29 @@ sub remove { my ( $self, $key ) = @_; $self->_init_db; - + my $id = _key($key); $self->{delete_sth}->execute($id); } sub purge { my ( $self ) = @_; - + my $dbh = $self->_init_db; - + + $dbh->sqlite_progress_handler(1000, sub { + main::idle(); + return; + }) if !main::SCANNER; + $dbh->do('DELETE FROM cache WHERE t >= 0 AND t < ' . time()); + + $dbh->sqlite_progress_handler(0, undef) if !main::SCANNER; } sub _key { my ( $key ) = @_; - + # Get a 60-bit unsigned int from MD5 (SQLite uses 64-bit signed ints for the key) # Have to concat 2 values here so it works on a 32-bit machine my $md5 = Digest::MD5::md5_hex($key); @@ -129,7 +136,7 @@ sub _key { sub pragma { my ( $self, $pragma ) = @_; - + if ( my $dbh = $self->_init_db ) { $dbh->do("PRAGMA $pragma"); @@ -142,7 +149,7 @@ sub pragma { sub close { my $self = shift; - + $self->_close_db; } @@ -161,7 +168,7 @@ my %_Expiration_Units = ( map(($_, 1), qw(s second seconds sec)), # of seconds from the present. E.g, "10 minutes" returns "600" sub _canonicalize_expiration_time { my ( $expiry ) = @_; - + if ( lc( $expiry ) eq 'now' ) { $expiry = 0; } @@ -183,15 +190,15 @@ sub _canonicalize_expiration_time { if ( $expiry <= 2592000 && $expiry > -1 ) { $expiry += time(); } - + return $expiry; } sub _get_dbfile { my $self = shift; - + my $namespace = $self->{namespace}; - + # namespace should not be longer than 8 characters on Windows, as it was causing DB corruption if ( main::ISWINDOWS && length($namespace) > 8 ) { $namespace = lc(substr($namespace, 0, 4)) . substr(Digest::MD5::md5_hex($namespace), 0, 4); @@ -200,7 +207,7 @@ sub _get_dbfile { elsif ( !main::ISWINDOWS ) { $namespace =~ s/\//-/g; } - + return catfile( $self->{root}, $namespace . '.db' ); } @@ -210,13 +217,13 @@ my $rebuilt; sub _init_db { my $self = shift; my $retry = shift; - + return $self->{dbh} if $self->{dbh}; - + my $dbfile = $self->_get_dbfile; - + my $dbh; - + eval { $dbh = DBI->connect( "dbi:SQLite:dbname=$dbfile", '', '', { AutoCommit => 1, @@ -232,22 +239,27 @@ sub _init_db { # only enable auto_vacuum when a file is newly created #$dbh->do('VACUUM'); } - + $dbh->do('PRAGMA synchronous = OFF'); $dbh->do('PRAGMA journal_mode = WAL'); # scanner is heavy on writes, server on reads - tweak accordingly $dbh->do('PRAGMA wal_autocheckpoint = ' . (main::SCANNER ? 10000 : 200)); - my $dbhighmem; + my ($dbhighmem, $dbjournalsize); if (main::RESIZER) { require Slim::Utils::Light; $dbhighmem = Slim::Utils::Light::getPref('dbhighmem'); + $dbjournalsize = Slim::Utils::Light::getPref('dbjournalsize'); } else { require Slim::Utils::Prefs; - $dbhighmem = Slim::Utils::Prefs::preferences('server')->get('dbhighmem'); + my $prefs = Slim::Utils::Prefs::preferences('server'); + $dbhighmem = $prefs->get('dbhighmem'); + $dbjournalsize = $prefs->get('dbjournalsize'); } - + + $dbh->do('PRAGMA journal_size_limit = ' . ($dbjournalsize * 1024 * 1024)) if defined $dbjournalsize; + # Increase cache size when using dbhighmem, and reduce it to 300K otherwise if ( $dbhighmem ) { $dbh->do('PRAGMA cache_size = 20000'); @@ -256,7 +268,7 @@ sub _init_db { else { $dbh->do('PRAGMA cache_size = 300'); } - + # Create the table, note that using an integer primary key # is much faster than any other kind of key, such as a char # because it doesn't have to create an index @@ -268,21 +280,21 @@ sub _init_db { $dbh->do('CREATE INDEX IF NOT EXISTS expiry ON cache (t)'); } }; - + if ( $@ ) { if ( $retry ) { # Give up after 2 tries die "Unable to read/create $dbfile\n"; } - + warn "$@Delete the file $dbfile and start from scratch.\n"; - + # Make sure cachedir exists Slim::Utils::Prefs::makeCacheDir() unless main::RESIZER; - + # Something was wrong with the database, delete it and try again unlink $dbfile; - + return $self->_init_db(1); } @@ -291,11 +303,11 @@ sub _init_db { my ($msg, $handle, $value) = @_; my $dbfile = $self->_get_dbfile; - + require Slim::Utils::Log; Slim::Utils::Log::logBacktrace($msg); Slim::Utils::Log::logError($dbfile); - + if ( $msg =~ /SQLite.*(?:database disk image is malformed|is not a database)/i ) { # we've already tried to recover - give up if ($rebuilt++) { @@ -309,7 +321,7 @@ sub _init_db { } } }; - + # Prepare statements we need if ($self->{noexpiry}) { $self->{set_sth} = $dbh->prepare('INSERT OR REPLACE INTO cache (k, v) VALUES (?, ?)'); @@ -320,22 +332,22 @@ sub _init_db { $self->{get_sth} = $dbh->prepare('SELECT v, t FROM cache WHERE k = ?'); } $self->{delete_sth} = $dbh->prepare('DELETE FROM cache WHERE k = ?'); - + $self->{dbh} = $dbh; - + return $dbh; } sub _close_db { my $self = shift; - + if ( $self->{dbh} ) { $self->{set_sth}->finish; $self->{get_sth}->finish; $self->{delete_sth}->finish; - + $self->{dbh}->disconnect; - + delete $self->{$_} for qw(set_sth get_sth dbh); } } diff --git a/Slim/Utils/Errno.pm b/Slim/Utils/Errno.pm index 3c2f7e00d55..66f5163b007 100644 --- a/Slim/Utils/Errno.pm +++ b/Slim/Utils/Errno.pm @@ -1,8 +1,7 @@ package Slim::Utils::Errno; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/Firmware.pm b/Slim/Utils/Firmware.pm index 7c99d9abf29..76daeac3e65 100644 --- a/Slim/Utils/Firmware.pm +++ b/Slim/Utils/Firmware.pm @@ -1,11 +1,10 @@ package Slim::Utils::Firmware; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. -# $Id$ =head1 NAME @@ -76,14 +75,14 @@ sub init { # Must initialize these here, not in declaration so that options have been parsed. $dir = Slim::Utils::OSDetect::dirsFor('Firmware'); $updatesDir = Slim::Utils::OSDetect::dirsFor('updates'); - + # clean up old download location Slim::Utils::Misc::deleteFiles($prefs->get('cachedir'), qr/^\w{4}_\d\.\d_.*\.bin(\.tmp)?$/i); Slim::Utils::Misc::deleteFiles($prefs->get('cachedir'), qr/^.*version$/i); # No longer try downloading all player firmwares at startup - just allow the background # download to get what is needed - + # Delete old ip3k firmware downloads - we should not normally need them again Slim::Utils::Misc::deleteFiles($updatesDir, qr/^(squeezebox|squeezebox2|transporter|boom|receiver)_\d+\.bin$/); } @@ -103,12 +102,12 @@ sub init_firmware_download { my $model = shift; return if $model eq 'squeezeplay'; # there is no firmware for the desktop version of squeezeplay! - + my $version_file = catdir( $updatesDir, "$model.version" ); my $custom_version = catdir( $updatesDir, "custom.$model.version" ); my $custom_image = catdir( $updatesDir, "custom.$model.bin" ); - + if ( -r $custom_version && -r $custom_image ) { main::INFOLOG && $log->info("Using custom $model firmware $custom_version $custom_image"); @@ -119,7 +118,7 @@ sub init_firmware_download { ($firmwares->{$model}->{version}, $firmwares->{$model}->{revision}) = $version =~ m/^([^ ]+)\sr(\d+)/; Slim::Web::Pages->addRawDownload("^firmware/custom.$model.bin", $custom_image, 'binary'); - + return; } @@ -154,15 +153,15 @@ in 1 day. sub init_version_done { my $version_file = shift; my $model = shift || 'jive'; - + my $version = read_file($version_file); - + # jive.version format: # 7.0 rNNNN # sdi@padbuild #24 Sat Sep 8 01:26:46 PDT 2007 my ($ver, $rev) = $version =~ m/^([^ ]+)\sr(\d+)/; - # on SqueezeOS we don't download firmware files + # on some systems we don't download firmware files # we'll let the player download them from squeezenetwork directly if ( Slim::Utils::OSDetect->getOS()->directFirmwareDownload() ) { @@ -174,14 +173,14 @@ sub init_version_done { Slim::Control::Request->new(undef, ['fwdownloaded', $model])->notify('firmwareupgrade'); } - + else { - + my $fw_file = catdir( $updatesDir, "${model}_${ver}_r${rev}.bin" ); if ( !-e $fw_file ) { main::INFOLOG && $log->info("Downloading $model firmware to: $fw_file"); - + downloadAsync( $fw_file, {cb => \&init_fw_done, pt => [$fw_file, $model]} ); } else { @@ -191,14 +190,14 @@ sub init_version_done { revision => $rev, file => $fw_file, }; - + Slim::Control::Request->new(undef, ['fwdownloaded', $model])->notify('firmwareupgrade'); - + Slim::Web::Pages->addRawDownload("^firmware/${model}.*\.bin", $fw_file, 'binary'); } } - + # Check again for an updated $model.version in 12 hours main::DEBUGLOG && $log->debug("Scheduling next $model.version check in " . ($prefs->get('checkVersionInterval') / 3600) . " hours"); Slim::Utils::Timers::setTimer( @@ -213,7 +212,7 @@ sub init_version_done { =head2 init_fw_done($fw_file, $model) Callback after firmware has been downloaded. Receives the filename -of the newly downloaded firmware and the $modelname. +of the newly downloaded firmware and the $modelname. Removes old firmware file if one exists. =cut @@ -221,11 +220,11 @@ Removes old firmware file if one exists. sub init_fw_done { my $fw_file = shift; my $model = shift; - + Slim::Utils::Misc::deleteFiles($updatesDir, qr/^$model.*\.bin(\.tmp)?$/i, $fw_file); - + my ($ver, $rev) = $fw_file =~ m/${model}_([^_]+)_r([^\.]+).bin/; - + $firmwares->{$model} = { version => $ver, revision => $rev, @@ -233,7 +232,7 @@ sub init_fw_done { }; main::DEBUGLOG && $log->debug("downloaded $ver $rev for $model - $fw_file"); - + Slim::Web::Pages->addRawDownload("^firmware/${model}.*\.bin", $fw_file, 'binary'); # send a notification that this firmware is downloaded @@ -252,21 +251,21 @@ sub init_fw_error { main::INFOLOG && $log->info("$model firmware download had an error"); get_fw_locally( $model ); - + # Note: Server will keep trying to download a new one } sub get_fw_locally { my $model = shift || 'jive'; - + for my $path ($updatesDir, $dir) { # Check if we have a usable Jive firmware my $version_file = catdir( $path, "$model.version" ); - + if ( -e $version_file ) { my $version = read_file($version_file); my ($ver, $rev) = $version =~ m/^([^ ]+)\sr(\d+)/; - + my $fw_file = catdir( $path, "${model}_${ver}_r${rev}.bin" ); if ( -e $fw_file ) { @@ -276,12 +275,12 @@ sub get_fw_locally { revision => $rev, file => $fw_file, }; - + Slim::Web::Pages->addRawDownload("^firmware/${model}.*\.bin", $fw_file, 'binary'); - + # send a notification that this firmware is downloaded Slim::Control::Request->new(undef, ['fwdownloaded', $model])->notify('firmwareupgrade'); - + last; } } @@ -306,7 +305,7 @@ sub url { return unless ($firmwares->{$model}->{file}); # Will be available immediately if custom f/w } - # when running on SqueezeOS, return the direct link from SqueezeNetwork + # on some systems return the direct link from SqueezeNetwork if ( Slim::Utils::OSDetect->getOS()->directFirmwareDownload() ) { return BASE() . $::VERSION . '/' . $model . '_' . $firmwares->{$model}->{version} @@ -315,7 +314,7 @@ sub url { } return unless $firmwares->{$model}->{file}; - + return Slim::Utils::Network::serverURL() . '/firmware/' . basename($firmwares->{$model}->{file}); } @@ -328,19 +327,19 @@ if there is no firmware downloaded. sub need_upgrade { my ( $class, $current, $model ) = @_; - + unless ($firmwares->{$model} && $firmwares->{$model}->{file} && $firmwares->{$model}->{version}) { main::DEBUGLOG && $log->debug("no firmware for $model - can't upgrade"); return; } - + my ($cur_version, $cur_rev) = $current =~ m/^([^ ]+)\sr(\d+)/; - + if ( !$cur_version || !$cur_rev ) { logError("$model sent invalid current version: $current"); return; } - + # Force upgrade if the version doesn't match, or if the rev is older # Allows newer firmware to work without forcing a downgrade if ( @@ -351,9 +350,9 @@ sub need_upgrade { main::DEBUGLOG && $log->debug("$model needs upgrade! (has: $current, needs: $firmwares->{$model}->{version} $firmwares->{$model}->{revision})"); return 1; } - + main::DEBUGLOG && $log->debug("$model doesn't need an upgrade (has: $current, server has: $firmwares->{$model}->{version} $firmwares->{$model}->{revision})"); - + return; } @@ -369,41 +368,41 @@ $file must be an absolute path. sub download { my ( $url, $file ) = @_; - + require LWP::UserAgent; my $ua = LWP::UserAgent->new( env_proxy => 1, ); - + my $error; - + msg("Downloading firmware from $url, please wait...\n"); - + my $res = $ua->mirror( $url, $file ); if ( $res->is_success ) { - + # Download the SHA1sum file to verify our download my $res2 = $ua->mirror( "$url.sha", "$file.sha" ); if ( $res2->is_success ) { - + my $sumfile = read_file( "$file.sha" ) or fatal("Unable to read $file.sha to verify firmware\n"); my ($sum) = $sumfile =~ m/([a-f0-9]{40})/; unlink "$file.sha"; - + open my $fh, '<', $file or fatal("Unable to read $file to verify firmware\n"); binmode $fh; - + my $sha1 = Digest::SHA1->new; $sha1->addfile($fh); close $fh; - + if ( $sha1->hexdigest eq $sum ) { logWarning("Successfully downloaded and verified $file."); return 1; } - + unlink $file; - + logError("Validation of firmware $file failed, SHA1 checksum did not match"); } else { @@ -414,12 +413,12 @@ sub download { else { $error = $res->status_line; } - + if ( $res->code == 304 ) { main::INFOLOG && $log->info("File $file not modified"); return 0; } - + logError("Unable to download firmware from $url: $error"); return 0; @@ -437,7 +436,7 @@ my %filesDownloading; sub downloadAsync { my ($file, $args) = @_; $args ||= {}; - + # Are we already downloading? my $callbacks; if (!$args->{'retry'} && ($callbacks = $filesDownloading{$file})) { @@ -448,13 +447,13 @@ sub downloadAsync { } return; } - + # Use an empty array ref as the default true value $filesDownloading{$file} ||= []; - + # URL to download my $url = BASE() . $::VERSION . '/' . basename($file); - + # Save to a tmp file so we can check SHA my $http = Slim::Networking::SimpleAsyncHTTP->new( \&downloadAsyncDone, @@ -465,9 +464,9 @@ sub downloadAsync { file => $file, }, ); - + main::INFOLOG && $log->info("Downloading in the background: $url -> $file"); - + $http->get( $url ); } @@ -482,12 +481,12 @@ sub downloadAsyncDone { my $args = $http->params(); my $file = $args->{'file'}; my $url = $http->url; - + # make sure we got the file if ( !-e "$file.tmp" ) { return downloadAsyncError( $http, 'File was not saved properly' ); } - + # Grab the SHA file, doesn't need to be saved to the filesystem $http = Slim::Networking::SimpleAsyncHTTP->new( \&downloadAsyncSHADone, @@ -497,7 +496,7 @@ sub downloadAsyncDone { saveAs => undef, } ); - + $http->get( $url . '.sha' ); } @@ -511,33 +510,33 @@ sub downloadAsyncSHADone { my $http = shift; my $args = $http->params(); my $file = $args->{'file'}; - + # get checksum my ($sum) = $http->content =~ m/([a-f0-9]{40})/; - + # open firmware file open my $fh, '<', "$file.tmp" or return downloadAsyncError( $http, "Unable to read $file to verify firmware" ); binmode $fh; - + my $sha1 = Digest::SHA1->new; $sha1->addfile($fh); close $fh; - + if ( $sha1->hexdigest eq $sum ) { - + # rename the tmp file rename "$file.tmp", $file or return downloadAsyncError( $http, "Unable to rename temporary $file file" ); - + main::INFOLOG && $log->info("Successfully downloaded and verified $file."); - + # reset back off time $CHECK_TIME = INITIAL_RETRY_TIME; - + my $cb = $args->{'cb'}; if ( $cb && ref $cb eq 'CODE' ) { $cb->( @{$args->{'pt'} || []} ); } - + # Pick up extra callbacks waiting for this file foreach $args (@{$filesDownloading{$file}}) { my $cb = $args->{'cb'}; @@ -545,7 +544,7 @@ sub downloadAsyncSHADone { $cb->( @{$args->{'pt'} || []} ); } } - + delete $filesDownloading{$file}; } else { @@ -565,10 +564,10 @@ sub downloadAsyncError { my $file = $http->params('file'); my $cb = $http->params('cb'); my $pt = $http->params('pt'); - + # Clean up unlink "$file.tmp" if -e "$file.tmp"; - + # If error was "Unable to open $file for writing", downloading will never succeed so just give up # Same for "Unable to write" if we run out of disk space, for example if ( $error =~ /Unable to (?:open|write)/ ) { @@ -586,8 +585,8 @@ sub downloadAsyncError { if ( my $proxy = $prefs->get('webproxy') ) { $log->error( sprintf("Please check your proxy configuration (%s)", $proxy) ); - } - + } + Slim::Utils::Timers::killTimers( $file, \&downloadAsync ); Slim::Utils::Timers::setTimer( $file, time() + $CHECK_TIME, \&downloadAsync, { @@ -597,14 +596,14 @@ sub downloadAsyncError { retry=> 1, }, ); - + # Increase retry time in case of multiple failures, but don't exceed MAX_RETRY_TIME $CHECK_TIME *= 2; if ( $CHECK_TIME > MAX_RETRY_TIME ) { $CHECK_TIME = MAX_RETRY_TIME; } } - + # Bug 9230, if we failed to download a Jive firmware but have a valid one in Cache already, # we should still offer it for download my $model = scalar @$pt > 1 ? $pt->[1] : 'jive'; @@ -621,9 +620,9 @@ Shuts down with an error message. sub fatal { my $msg = shift; - + logError($msg); - + main::stopServer(); } diff --git a/Slim/Utils/GDResizer.pm b/Slim/Utils/GDResizer.pm index b0fc0a715d8..ecf2fa28d08 100644 --- a/Slim/Utils/GDResizer.pm +++ b/Slim/Utils/GDResizer.pm @@ -32,7 +32,7 @@ Returns an array with the resized image data as a scalar ref, and the image form sub resize { my ( $class, %args ) = @_; - + my $origref = $args{original}; my $file = $args{file}; my $format = $args{format}; @@ -40,33 +40,33 @@ sub resize { my $height = $args{height} || 'X'; my $bgcolor = $args{bgcolor}; my $mode = $args{mode}; - + $debug = $args{debug} if defined $args{debug}; - + # if $file is a scalar ref, then it's the image data itself if ( ref $file && ref $file eq 'SCALAR' ) { $origref = $file; $file = undef; } - + my ($offset, $length) = (0, 0); # used if an audio file is passed in my $in_format; - + if ( $file && !-e $file ) { die "Unable to resize from $file: File does not exist\n"; } - + # Load image data from tags if necessary if ( $file && $file !~ /\.(?:jpe?g|gif|png)$/i ) { # Double-check that this isn't an image file if ( !_content_type_file($file, 0, 1) ) { ($offset, $length, $origref) = _read_tag($file); - + if ( !$offset ) { if ( !$origref ) { die "Unable to find any image tag in $file\n"; } - + $file = undef; } # sometimes we get an invalid offset, but Audio::Scan is able to read the image data anyway @@ -77,47 +77,47 @@ sub resize { } } } - + # Remember if user requested a specific format my $explicit_format = $format; - + # Format of original image $in_format ||= $file ? _content_type_file($file, $offset) : _content_type($origref); - + # Ignore width/height of 'X' $width = undef if $width eq 'X'; $height = undef if $height eq 'X'; - + # Short-circuit if no width/height specified, and formats match, return original image if ( !$width && !$height ) { if ( !$explicit_format || ($explicit_format eq $in_format) ) { return $file ? (_slurp($file, $length ? $offset : undef, $length || undef), $in_format) : ($origref, $in_format); } } - + # Abort if invalid params if ( ($width && $width !~ /^\d+$/) || ($height && $height !~ /^\d+$/) ) { return $file ? (_slurp($file, $length ? $offset : undef, $length || undef), $in_format) : ($origref, $in_format); } - + # Fixup bgcolor and convert from hex if ( $bgcolor && length($bgcolor) != 6 && length($bgcolor) != 8 ) { $bgcolor = 0xFFFFFF; } - + if ( !$mode ) { # default mode is always max $mode = 'm'; } - + if ( $debug && $file ) { warn "Loading image from $file\n"; } - + my $im = $file ? Image::Scale->new( $file, { offset => $offset, length => $length } ) : Image::Scale->new($origref); - + my ($in_width, $in_height) = ($im->width, $im->height); # Output format @@ -129,14 +129,14 @@ sub resize { $format = 'png'; } } - + if ( !$width && !$height ) { $width = $in_width; $height = $in_height; } - + main::idleStreams() unless main::RESIZER; - + if ( $mode eq 'm' || $mode eq 'p' ) { # Bug 17140, switch to png if image will contain any padded space if ( $format ne 'png' ) { @@ -159,14 +159,13 @@ sub resize { $height = $in_height; } } - + $debug && warn "Resizing from ${in_width}x${in_height} $in_format @ ${offset} to ${width}x${height} $format\n"; - + $im->resize( { width => $width, height => $height, keep_aspect => 1, - # XXX memory_limit on SqueezeOS } ); } @@ -178,7 +177,6 @@ sub resize { width => $in_width, height => $in_height, keep_aspect => 1, - # XXX memory_limit on SqueezeOS } ); # Requested size is smaller than original -> resize to requested size } else { @@ -187,7 +185,6 @@ sub resize { width => $width, height => $height, keep_aspect => 1, - # XXX memory_limit on SqueezeOS } ); } } @@ -203,15 +200,14 @@ sub resize { $width = $in_width; } } - + $im->resize( { width => $width, - # XXX memory_limit on SqueezeOS } ); } - + main::idleStreams() unless main::RESIZER; - + my $out; if ( $format eq 'png' ) { @@ -222,16 +218,16 @@ sub resize { $out = $im->as_jpeg; $format = 'jpg'; } - + return (\$out, $format); } sub getSize { my $class = shift; my $ref = shift; - + my $im = Image::Scale->new($ref) || return (0, 0); - + return ($im->width, $im->height); } @@ -254,67 +250,67 @@ Returns arrayref of [ resized image data as a scalar ref, image format, width, h sub resizeSeries { my ( $class, %args ) = @_; - + my @series = sort { $b->{width} <=> $a->{width} } @{ delete $args{series} }; $debug = $args{debug} if defined $args{debug}; - + my @ret; - + for my $next ( @series ) { $args{width} = $next->{width}; $args{height} = $next->{height} || $next->{width}; $args{mode} = $next->{mode} if $next->{mode}; - + $debug && warn "Resizing series: " . $args{width} . 'x' . $args{height} . "\n"; - + my ($resized_ref, $format) = $class->resize( %args ); - + delete $args{file}; - + # Don't use source artwork < 100, as this results in blurry images if ( !$args{original} || ($args{width} >= 100 && $args{height} >= 100) ) { $args{original} = $resized_ref; } - + push @ret, [ $resized_ref, $format, $args{width}, $args{height}, $args{mode} ]; } - + return wantarray ? @ret : \@ret; } sub _read_tag { my $file = shift; - + my ($offset, $length); - + require Audio::Scan; - + # First try to get offset/length if possible - + local $ENV{AUDIO_SCAN_NO_ARTWORK} = 1; - + $debug && warn "Reading tags from audio file...\n"; - + my $s = eval { Audio::Scan->scan_tags($file) }; if ( $@ ) { die "Unable to read image tag from $file: $@\n"; } - + my $tags = $s->{tags}; - + # MP3, other files with ID3v2 if ( my $pic = $tags->{APIC} ) { if ( ref $pic->[0] eq 'ARRAY' ) { # multiple images, return image with lowest image_type value $pic = ( sort { $a->[1] <=> $b->[1] } @{$pic} )[0]; } - + if ( $pic->[4] ) { # offset is available return ( $pic->[4], $pic->[3] ); } } - + # FLAC/Vorbis picture block if ( $tags->{ALLPICTURES} ) { my $pic = ( sort { $a->{picture_type} <=> $b->{picture_type} } @{ $tags->{ALLPICTURES} } )[0]; @@ -322,27 +318,27 @@ sub _read_tag { return ( $pic->{offset}, $pic->{image_data} ); } } - + # ALAC/M4A if ( $tags->{COVR} ) { return ( $tags->{COVR_offset}, $tags->{COVR} ); } - + # WMA if ( my $pic = $tags->{'WM/Picture'} ) { if ( ref $pic eq 'ARRAY' ) { # return image with lowest image_type value $pic = ( sort { $a->{image_type} <=> $b->{image_type} } @{$pic} )[0]; } - + return ( $pic->{offset}, $pic->{image} ); } - + # APE if ( $tags->{'COVER ART (FRONT)'} ) { return ( $tags->{'COVER ART (FRONT)_offset'}, $tags->{'COVER ART (FRONT)'} ); } - + # Escient artwork app block (who uses this??) if ( $tags->{APPLICATION} && $tags->{APPLICATION}->{1163084622} ) { my $artwork = $tags->{APPLICATION}->{1163084622}; @@ -350,64 +346,64 @@ sub _read_tag { return (undef, undef, \$artwork); } } - + # We get here if the embedded image is either ID3 APIC with unsync null bytes, or a Vorbis base64 tag # In this case we need to re-read the full artwork using Audio::Scan - + return _read_data_from_tag($file); } sub _read_data_from_tag { my $file = shift; - + local $ENV{AUDIO_SCAN_NO_ARTWORK} = 0; - + $debug && warn "Offset information not found or invalid, re-reading file for direct artwork\n"; - + my $s = Audio::Scan->scan_tags($file); my $tags = $s->{tags}; - + # MP3, other files with ID3v2 if ( my $pic = $tags->{APIC} ) { if ( ref $pic->[0] eq 'ARRAY' ) { # multiple images, return image with lowest image_type value $pic = ( sort { $a->[1] <=> $b->[1] } @{$pic} )[0]; } - + return ( undef, undef, \($pic->[3]) ); } - + # Vorbis picture block if ( $tags->{ALLPICTURES} ) { my $pic = ( sort { $a->{picture_type} <=> $b->{picture_type} } @{ $tags->{ALLPICTURES} } )[0]; return ( undef, undef, \($pic->{image_data}) ); } - + return; } sub _content_type_file { my $file = shift; my $offset = shift; - + open my $fh, '<', $file; binmode $fh; - + if ($offset) { sysseek $fh, $offset, 0; } - + sysread $fh, my $buf, 8; close $fh; - + return _content_type(\$buf, @_); } sub _content_type { my ( $dataref, $silent ) = @_; - + my $ct; - + my $magic = substr $$dataref, 0, 8; if ( $magic =~ /^\x89PNG\x0d\x0a\x1a\x0a/ ) { $ct = 'png'; @@ -427,10 +423,10 @@ sub _content_type { require Data::Dump; die "Can't resize unknown type, magic: " . Data::Dump::dump($magic) . "\n"; } - + return; } - + return $ct; } @@ -438,10 +434,10 @@ sub _slurp { my $file = shift; my $offset = shift; my $data; - + open my $fh, '<', $file or die "Cannot open $file"; binmode $fh; - + if ( defined $offset ) { my $length = shift; # Read only a portion of the file @@ -456,42 +452,42 @@ sub _slurp { # Read entire file $data = do { local $/; <$fh> }; } - + close $fh; - + return \$data; } sub gdresize { my ($class, %args) = @_; - + my $file = $args{file}; my $spec = $args{spec}; my $cache = $args{cache}; my $cachekey = $args{cachekey}; - + $debug = $args{debug}; - + if ( @$spec > 1 ) { # Resize in series - + # Construct spec hashes my $specs = []; for my $s ( @$spec ) { my ($width, $height, $mode) = $s =~ /^([^x]+)x([^_]+)_(\w)$/; - + if ( !$width || !$height || !$mode ) { warn "Invalid spec: $s\n"; next; } - + push @{$specs}, { width => $width, height => $height, mode => $mode, }; } - + my $series = eval { $class->resizeSeries( file => $file, @@ -499,31 +495,31 @@ sub gdresize { debug => $debug, ); }; - + if ( $@ ) { die "$@\n"; } - + if ( $cache && $cachekey ) { for my $s ( @{$series} ) { my $width = $s->[2]; my $height = $s->[3]; my $mode = $s->[4]; - + # Series-based resize has to append to the cache key my $key = $cachekey; my $spec = "${width}x${height}_${mode}"; if (! ($key =~ s/(\.\w{3,4})$/_$spec$1/)) { $key .= $spec; } - + _cache( $cache, $key, $s->[0], $file, $s->[1] ); } } } else { my ($width, $height, $mode, $bgcolor, $ext) = $spec->[0] =~ /^(?:([0-9X]+)x([0-9X]+))?(?:_(\w))?(?:_([\da-fA-F]+))?(?:\.(\w+))?$/; - + # XXX If cache is available, pull pre-cached size values from cache # to see if we can use a smaller version of this image than the source # to reduce resizing time. @@ -539,14 +535,14 @@ sub gdresize { format => $ext, debug => $debug, ); - + $file = undef if ref $file; }; - + if ( $@ ) { die "$@\n"; } - + if ( $cache && $cachekey ) { # When doing a single resize, the cachekey passed in is all we store # XXX Don't cache images that aren't resized, i.e. /cover.jpg @@ -559,7 +555,7 @@ sub gdresize { sub _cache { my ( $cache, $key, $imgref, $file, $ct ) = @_; - + my $cached = { content_type => $ct, mtime => $file ? (stat($file))[9] : 0, @@ -568,7 +564,7 @@ sub _cache { }; $cache->set( $key, $cached ); - + $debug && warn "Cached $key (" . length($$imgref) . " bytes)\n"; } diff --git a/Slim/Utils/IPDetect.pm b/Slim/Utils/IPDetect.pm index 1764342fed2..4fe3dc58422 100644 --- a/Slim/Utils/IPDetect.pm +++ b/Slim/Utils/IPDetect.pm @@ -1,8 +1,6 @@ package Slim::Utils::IPDetect; -# $Id:$ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -12,8 +10,14 @@ use Socket qw(inet_aton inet_ntoa sockaddr_in pack_sockaddr_in PF_INET SOCK_DGRA use Symbol; use Slim::Utils::Log; -my $detectedIP = undef; -my $localhost = '127.0.0.1'; +use constant RETRY_AFTER => 60; +use constant MAX_ATTEMPTS => 5; +use constant LOCALHOST => '127.0.0.1'; + +my $detectedIP; +my $gateway; +my $lastCheck = 0; +my $checkCount = 0; =head1 NAME @@ -40,6 +44,9 @@ L =cut sub IP { + if ($detectedIP && $detectedIP eq LOCALHOST && $checkCount <= MAX_ATTEMPTS && time() - $lastCheck > RETRY_AFTER) { + $detectedIP = undef; + } if (!$detectedIP) { _init(); @@ -52,7 +59,25 @@ sub IP_port { return IP() . ':' . $main::SLIMPROTO_PORT; } + +=head1 defaultGateway() + +Returns the IP address of the system's default gateway, or an empty if not found + +=cut + +sub defaultGateway { + if (!defined $gateway) { + $gateway = Slim::Utils::OSDetect->getOS()->getDefaultGateway() || ''; + $gateway = '' if !Slim::Utils::Network::ip_is_ipv4($gateway); + } + + return $gateway; +} + sub _init { + $lastCheck = time(); + $checkCount++; if ($detectedIP) { return; @@ -76,9 +101,9 @@ sub _init { my $iaddr = inet_aton($raddr) || do { - logWarning("Couldn't call inet_aton($raddr) - falling back to $localhost"); + logWarning("Couldn't call inet_aton($raddr) - falling back to " . LOCALHOST); - $detectedIP = $localhost; + $detectedIP = LOCALHOST; return; }; @@ -87,9 +112,9 @@ sub _init { socket($sock, PF_INET, SOCK_DGRAM, $proto) || do { - logWarning("Couldn't call socket(PF_INET, SOCK_DGRAM, \$proto) - falling back to $localhost"); + logWarning("Couldn't call socket(PF_INET, SOCK_DGRAM, \$proto) - falling back to " . LOCALHOST); - $detectedIP = $localhost; + $detectedIP = LOCALHOST; return; }; @@ -100,9 +125,9 @@ sub _init { bind($sock, pack_sockaddr_in(0, $laddr)) or do { - logWarning("Couldn't call bind(pack_sockaddr_in(0, \$laddr) - falling back to $localhost"); + logWarning("Couldn't call bind(pack_sockaddr_in(0, \$laddr) - falling back to " . LOCALHOST); - $detectedIP = $localhost; + $detectedIP = LOCALHOST; return; }; @@ -110,9 +135,9 @@ sub _init { connect($sock, $paddr) || do { - logWarning("Couldn't call connect() - falling back to $localhost"); + logWarning("Couldn't call connect() - falling back to " . LOCALHOST); - $detectedIP = $localhost; + $detectedIP = LOCALHOST; return; }; diff --git a/Slim/Utils/ImageResizer.pm b/Slim/Utils/ImageResizer.pm index 849871e95d3..adf6ff9fde2 100644 --- a/Slim/Utils/ImageResizer.pm +++ b/Slim/Utils/ImageResizer.pm @@ -3,6 +3,7 @@ package Slim::Utils::ImageResizer; use strict; use File::Spec::Functions qw(catdir); +use MIME::Base64 qw(encode_base64); use Scalar::Util qw(blessed); use Slim::Utils::ArtworkCache; @@ -21,93 +22,99 @@ my $log = logger('artwork'); my ($gdresizein, $gdresizeout, $gdresizeproc); my $pending_requests = 0; -my $hasDaemon; +my $hasDaemon; sub hasDaemon { - if (!defined $hasDaemon) { - $hasDaemon = !main::SCANNER && !main::ISWINDOWS && -r SOCKET_PATH && -w _; + my ($class, $check) = @_; + + if (!defined $hasDaemon || $check) { + $hasDaemon = (!main::SCANNER && !main::ISWINDOWS && -r SOCKET_PATH && -w _) or do { + unlink SOCKET_PATH; + }; } - + return $hasDaemon; } sub resize { my ($class, $file, $cachekey, $specs, $callback, $cache) = @_; - + my $isDebug = main::DEBUGLOG && $log->is_debug; - + # Check for callback, and that the gdresized daemon running and read/writable if (hasDaemon() && $callback) { require AnyEvent::Socket; require AnyEvent::Handle; - + # Get cache root for passing to daemon $cache ||= Slim::Utils::ArtworkCache->new(); my $cacheroot = $cache->getRoot(); - + main::DEBUGLOG && $isDebug && $log->debug("Using gdresized daemon to resize (pending requests: $pending_requests)"); - + $pending_requests++; - + # Daemon available, do an async resize AnyEvent::Socket::tcp_connect( 'unix/', SOCKET_PATH, sub { my $fh = shift || do { - main::DEBUGLOG && $isDebug && $log->debug("daemon failed to connect: $!"); - + $log->error("daemon failed to connect: $!"); + if ( --$pending_requests == 0 ) { main::DEBUGLOG && $isDebug && $log->debug("no more pending requests"); } - + # Fallback to resizing the old way sync_resize($file, $cachekey, $specs, $callback, $cache); - + return; }; - + my $handle; - + # Timer in case daemon craps out my $timeout = sub { - main::DEBUGLOG && $isDebug && $log->debug("daemon timed out"); - + $log->error("daemon timed out"); + $handle && $handle->destroy; - + if ( --$pending_requests == 0 ) { main::DEBUGLOG && $isDebug && $log->debug("no more pending requests"); } - + # Fallback to resizing the old way sync_resize($file, $cachekey, $specs, $callback, $cache); }; Slim::Utils::Timers::setTimer( undef, Time::HiRes::time() + SOCKET_TIMEOUT, $timeout ); - + $handle = AnyEvent::Handle->new( fh => $fh, on_read => sub {}, on_eof => undef, on_error => sub { my $result = delete $_[0]->{rbuf}; - + main::DEBUGLOG && $isDebug && $log->debug("daemon result: $result"); - + $_[0]->destroy; - + Slim::Utils::Timers::killTimers(undef, $timeout); - + if ( --$pending_requests == 0 ) { main::DEBUGLOG && $isDebug && $log->debug("no more pending requests"); } - + $callback && $callback->(); }, ); - - $handle->push_write( pack('Z*Z*Z*Z*', $file, $specs, $cacheroot, $cachekey) . "\015\012" ); + + main::INFOLOG && $log->is_info && $log->info(sprintf("file=%s, spec=%s, cacheroot=%s, cachekey=%s, imagedata=%s bytes\n", ref $file ? 'data' : $file, $specs, $cacheroot, $cachekey, (ref $file ? length($$file) : 0))); + + $handle->push_write( pack('Z* Z* Z* Z* Z*', ref $file ? 'data' : $file, $specs, $cacheroot, $cachekey, (ref $file ? encode_base64($$file, '') : '')) . "\015\012" ); }, sub { # prepare callback, used to set the timeout return SOCKET_TIMEOUT; } ); - + return; } else { @@ -118,13 +125,13 @@ sub resize { sub sync_resize { my ( $file, $cachekey, $specs, $callback, $cache ) = @_; - + require Slim::Utils::GDResizer; - + my $isDebug = main::DEBUGLOG && $log->is_debug; - + my ($ref, $format); - + my @spec = split(',', $specs); eval { ($ref, $format) = Slim::Utils::GDResizer->gdresize( @@ -135,14 +142,14 @@ sub sync_resize { debug => $isDebug, ); }; - + if ( main::DEBUGLOG && $isDebug && $@ ) { $file = '' if ref $file; $log->error("Error resizing $file: $@"); } - + $callback && $callback->($ref, $format); - + return $@ ? 0 : 1; } diff --git a/Slim/Utils/Light.pm b/Slim/Utils/Light.pm index 28d44233902..4f5255c8dc8 100644 --- a/Slim/Utils/Light.pm +++ b/Slim/Utils/Light.pm @@ -1,8 +1,6 @@ package Slim::Utils::Light; -# $Id: $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/Log.pm b/Slim/Utils/Log.pm index 1e9f1121a88..b3ae84200f9 100644 --- a/Slim/Utils/Log.pm +++ b/Slim/Utils/Log.pm @@ -1,10 +1,9 @@ package Slim::Utils::Log; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Dan Sully, Logitech. +# Logitech Media Server Copyright 2001-2020 Dan Sully, Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. =head1 NAME @@ -93,7 +92,7 @@ sub init { # call poor man's log rotation if (!main::SCANNER) { - + Slim::Utils::OSDetect::getOS->logRotate($logDir); } @@ -130,7 +129,7 @@ sub init { $config{'log4perl.rootLogger'} = join(', ', @levels); } - + # Make sure recreate option is set if user has an existing log.conf if ( !main::ISWINDOWS && !$ENV{NYTPROF} ) { $config{'log4perl.appender.server.recreate'} = 1; @@ -140,11 +139,11 @@ sub init { $config{'log4perl.appender.server.recreate'} = 0; $config{'log4perl.appender.server.recreate_check_signal'} = ''; } - + # Change to syslog if requested if ( $args->{logfile} && $args->{logfile} eq 'syslog' ) { delete $config{$_} for grep { /^log4perl.appender/ } keys %config; - + %config = (%config, $class->_syslogAppenders); } @@ -415,7 +414,7 @@ sub setLogLevelForCategory { =head2 isValidCategory ( category ) -Returns true if the passed category is valid. +Returns true if the passed category is valid. Returns false otherwise. @@ -820,6 +819,9 @@ sub logGroups { 'database.info' => 'DEBUG', 'plugin.itunes' => 'DEBUG', 'plugin.musicip' => 'DEBUG', + 'database.virtuallibraries' => 'DEBUG', + 'formats.audio' => 'DEBUG', + 'formats.playlists' => 'DEBUG', }, label => 'DEBUG_SCANNER_CHOOSE', }, @@ -829,25 +831,25 @@ sub logGroups { # logging options we want to pass to the scanner sub getScannerLogOptions { my $class = shift; - + my $options = $class->logGroups()->{SCANNER}->{categories}; my $defaults = $class->logLevels(); - + foreach my $key (keys %$options) { $options->{$key} = $runningConfig{"log4perl.logger.$key"} || $defaults->{$key}; - + } - + return $options; } sub setLogGroup { my ($class, $group, $persist) = @_; - + my $levels = $class->logLevels($group); my $categories = $class->allCategories(); - + for my $category (keys %{$categories}) { $class->setLogLevelForCategory( $category, $levels->{$category} || 'ERROR' @@ -860,7 +862,7 @@ sub setLogGroup { sub logLevels { my $group = $_[1]; - + my $categories = { 'server' => 'ERROR', 'server.memory' => 'OFF', @@ -868,6 +870,7 @@ sub logLevels { 'server.scheduler' => 'ERROR', 'server.select' => 'ERROR', 'server.timers' => 'ERROR', + 'server.update' => 'ERROR', 'artwork' => 'ERROR', 'artwork.imageproxy' => 'ERROR', @@ -901,7 +904,7 @@ sub logLevels { 'control.command' => 'ERROR', 'control.queries' => 'ERROR', 'control.stdio' => 'ERROR', - + 'menu.trackinfo' => 'ERROR', 'player.alarmclock' => 'ERROR', @@ -919,6 +922,7 @@ sub logLevels { 'player.sync' => 'ERROR', 'player.text' => 'ERROR', 'player.ui' => 'ERROR', + 'player.ui.screensaver' => 'ERROR', 'scan' => 'ERROR', 'scan.auto' => 'DEBUG', # XXX forced on because there will be problems @@ -929,11 +933,11 @@ sub logLevels { 'perfmon' => 'WARN, screen-raw, perfmon', # perfmon assumes this is set to WARN }; - + $categories->{'network.squeezenetwork'} = 'ERROR' unless main::NOMYSB; return $categories unless $group; - + my $logGroups = logGroups(); foreach (keys %{ $logGroups->{$group}->{categories} }) { @@ -1013,7 +1017,7 @@ sub _defaultAppenders { sub _syslogAppenders { my $class = shift; - + eval { require Log::Dispatch::Syslog }; if ( $@ ) { die "Unable to enable syslog support: $@\n"; diff --git a/Slim/Utils/MemoryUsage.pm b/Slim/Utils/MemoryUsage.pm index 0bb92cede9a..6f8b10e3c46 100644 --- a/Slim/Utils/MemoryUsage.pm +++ b/Slim/Utils/MemoryUsage.pm @@ -1,6 +1,5 @@ package Slim::Utils::MemoryUsage; -# $Id$ # # This module is a merging of B::TerseSize and Apache::Status # put together to work with Logitech Media Server by Dan Sully diff --git a/Slim/Utils/Misc.pm b/Slim/Utils/Misc.pm index 5aa8764c015..4c2d1783c0c 100644 --- a/Slim/Utils/Misc.pm +++ b/Slim/Utils/Misc.pm @@ -1,8 +1,7 @@ package Slim::Utils::Misc; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -144,7 +143,13 @@ sub addFindBinPaths { while (my $path = shift) { - if (-d $path) { + # don't register duplicate entries + if (grep { $_ eq $path } @findBinPaths) { + + main::INFOLOG && $ospathslog->is_info && $ospathslog->info("not adding $path - duplicate entry"); + + } + elsif (-d $path) { main::INFOLOG && $ospathslog->is_info && $ospathslog->info("adding $path"); @@ -408,12 +413,12 @@ sub crackURL { my $urlstring = join('|', Slim::Player::ProtocolHandlers->registeredHandlers); - $string =~ m|(?:$urlstring)://(?:([^\@:]+):?([^\@]*)\@)?([^:/]+):*(\d*)(\S*)|i; + $string =~ m|(?:$urlstring)://(?:([^\@\/:]+):?([^\@\/]*)\@)?([^:/]+):*(\d*)(\S*)|i; my ($user, $pass, $host, $port, $path) = ($1, $2, $3, $4, $5); $path ||= '/'; - $port ||= 80; + $port ||= ((Slim::Networking::Async::HTTP->hasSSL() && $string =~ /^https/) ? 443 : 80); if ( main::DEBUGLOG && $ospathslog->is_debug ) { $ospathslog->debug("Cracked: $string with [$host],[$port],[$path]"); @@ -656,7 +661,9 @@ sub getMediaDirs { }->{$type}) } }; $mediadirs = [ grep { !$ignoreList->{$_} } @$mediadirs ]; - $mediadirs = [ grep /^\Q$filter\E$/, @$mediadirs] if $filter; + $mediadirs = [ grep { $_ } map { + ($filter eq $_ || $filter =~ /^\Q$_\E/) && $filter + } @$mediadirs] if $filter; } $mediadirsCache{$type} = [ map { $_ } @$mediadirs ] unless $filter; @@ -906,6 +913,10 @@ sub readDirectory { return @diritems; } + + # At some point Windows seems to have started returning content of the "current directory" on the drive + # if the path wasn't absolute. Make sure we start with a slash if only a drive letter is given. - mh + $native_dirname .= '/' if $native_dirname =~ /^[a-z]:$/i; } if ($recursive) { @@ -926,7 +937,7 @@ sub readDirectory { while (defined (my $item = readdir(DIR)) ) { # call idle streams to service timers - used for blocking animation. - if (scalar @diritems % 3) { + if (!scalar @diritems % 20) { main::idleStreams(); } @@ -1115,7 +1126,7 @@ sub parseRevision { # if we're running from a git clone, report the last commit ID and timestamp # "git -C ..." is only available in recent git version, more recent than what CentOS provides... - if ( $revision eq 'TRUNK' && `cd $Bin && git show -s --format=%h\\|%ci 2> /dev/null` =~ /^([0-9a-f]+)\|(\d{4}-\d\d-\d\d.*)/i ) { + if ( !main::ISWINDOWS && $revision eq 'TRUNK' && `cd $Bin && git show -s --format=%h\\|%ci 2> /dev/null` =~ /^([0-9a-f]+)\|(\d{4}-\d\d-\d\d.*)/i ) { $revision = 'git-' . $1; $builddate = $2; } diff --git a/Slim/Utils/MySQLHelper.pm b/Slim/Utils/MySQLHelper.pm index ff533a66491..5e08a9b7472 100644 --- a/Slim/Utils/MySQLHelper.pm +++ b/Slim/Utils/MySQLHelper.pm @@ -1,6 +1,5 @@ package Slim::Utils::MySQLHelper; -# $Id$ =head1 NAME diff --git a/Slim/Utils/Network.pm b/Slim/Utils/Network.pm index a929d635e61..3a323d1332e 100644 --- a/Slim/Utils/Network.pm +++ b/Slim/Utils/Network.pm @@ -1,8 +1,7 @@ package Slim::Utils::Network; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -154,6 +153,38 @@ sub serverAddr { return $main::httpaddr || hostAddr(); } +=head2 serverMACAddress + +Returns the MAC address the server is listening on (if possible). + +This isn't trying as hard as eg. Net::Address::Ethernet, as arp etc. +often would take too much time on the many disconnected interfaces +of nowadays computers. In particular macOS Sierra seems to cause issues. + +=cut + +sub serverMACAddress { + my $addresses; + eval { + require Net::Ifconfig::Wrapper; + $addresses = Net::Ifconfig::Wrapper::Ifconfig('list'); + + # we're only interested in interfaces which have a known MAC and IP address + $addresses = [ grep { $_->{inet} && $_->{ether} } values %$addresses ]; + }; + + if ($addresses) { + my $hostAddr = serverAddr(); + my ($address) = grep { $_->{inet}->{$hostAddr} } @$addresses; + + # if we didn't find our IP address, then let's pick just one of the list + $address ||= $addresses->[0]; + + return $address->{ether}; + } +} + + =head2 serverURL( ) Return the base URL for this server @@ -414,6 +445,60 @@ sub ip_is_ipv4 { return 1; } +=head2 ip_is_host() + +Checks whether given IP address is the host's address or localhost + +=cut + +sub ip_is_host { + my $ip = shift || return; + + return 1 if $ip eq '127.0.0.1'; + return 1 if $ip eq serverAddr(); + + return intip($ip) == intip(serverAddr()) ? 1 : 0; +} + +=head1 ip_is_gateway($ip) + +Try to figure out whether an IP address is the host's gateway + +=cut + +sub ip_is_gateway { + my ($ip) = @_; + + # Check for invalid chars + return unless ip_is_ipv4($ip); + + my $gateway = Slim::Utils::IPDetect::defaultGateway(); + + return unless $gateway; + + return intip($ip) == intip($gateway) ? 1 : 0; +} + +=head1 ip_on_different_network($ip) + +Try to figure out whether an IP address is on the same network as Logitech Media Server. +It's very simplistic in that it only checks whether we're in a private network, but the +requested IP is not (and vice versa) + +=cut + +sub ip_on_different_network { + my ($ip) = @_; + + # Check for invalid chars + return unless ip_is_ipv4($ip); + + # if our host IP is 127.0.0.1 (lookup failed), then all networks would be different - ignore + return if hostAddr() eq '127.0.0.1'; + + return ip_is_private(serverAddr()) ? !ip_is_private($ip) : ip_is_private($ip); +} + =head1 SEE ALSO L diff --git a/Slim/Utils/OS.pm b/Slim/Utils/OS.pm index 1ab40f07041..a429ea4e19c 100644 --- a/Slim/Utils/OS.pm +++ b/Slim/Utils/OS.pm @@ -1,10 +1,8 @@ package Slim::Utils::OS; -# $Id: Base.pm 21790 2008-07-15 20:18:07Z andy $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. # Base class for OS specific code @@ -23,7 +21,7 @@ sub new { my $self = { osDetails => {}, }; - + return bless $self, $class; } @@ -31,6 +29,19 @@ sub initDetails { return shift->{osDetails}; } +sub getMACAddress { + my $class = shift; + + if (!main::SCANNER && !defined $class->{osDetails}->{mac}) { + require Slim::Utils::Network; + + # fall back to empty string to prevent repeated attempts (if needed) + $class->{osDetails}->{mac} = Slim::Utils::Network::serverMACAddress() || ''; + } + + return $class->{osDetails}->{mac}; +} + sub details { return shift->{osDetails}; } @@ -49,31 +60,33 @@ Windows & OSX handle this in the installer sub migratePrefsFolder {} -sub sqlHelperClass { +sub sqlHelperClass { if ( $main::dbtype ) { return "Slim::Utils::${main::dbtype}Helper"; } - + return 'Slim::Utils::SQLiteHelper'; } # Skip obsolete plugins, they should be deleted by installers sub skipPlugins {return (qw(Picks ShoutcastBrowser Webcasters Health));} -=head2 initSearchPath( ) +=head2 initSearchPath( [$baseDir] ) -Initialises the binary seach path used by Slim::Utils::Misc::findbin to OS specific locations +Initialises the binary seach path used by Slim::Utils::Misc::findbin to OS specific locations. +Optionally a base directory can be defined, eg. used to add plugin specific folders. =cut sub initSearchPath { my $class = shift; + my $baseDir = shift || $class->dirsFor('Bin'); # Initialise search path for findbin - called later in initialisation than init above - # Reduce all the x86 architectures down to i386, including x86_64, so we only need one directory per *nix OS. + # Reduce all the x86 architectures down to i386, including x86_64, so we only need one directory per *nix OS. my $binArch = $class->{osDetails}->{'binArch'} = $Config::Config{'archname'}; $class->{osDetails}->{'binArch'} =~ s/^(?:i[3456]86|x86_64)-([^-]+).*/i386-$1/; - + # Reduce ARM to arm(hf)-linux if ( $class->{osDetails}->{'binArch'} =~ /^arm.*linux.*gnueabihf/ || ($class->{osDetails}->{'binArch'} =~ /arm/ && ( @@ -86,22 +99,28 @@ sub initSearchPath { elsif ( $class->{osDetails}->{'binArch'} =~ /^arm.*linux/ ) { $class->{osDetails}->{'binArch'} = 'arm-linux'; } - + elsif ( $class->{osDetails}->{'binArch'} =~ /^aarch64-linux/ ) { + $class->{osDetails}->{'binArch'} = 'aarch64-linux'; + } + # Reduce PPC to powerpc-linux - if ( $class->{osDetails}->{'binArch'} =~ /^(?:ppc|powerpc).*linux/ ) { + elsif ( $class->{osDetails}->{'binArch'} =~ /^(?:ppc|powerpc).*linux/ ) { $class->{osDetails}->{'binArch'} = 'powerpc-linux'; } - my @paths = ( catdir($class->dirsFor('Bin'), $class->{osDetails}->{'binArch'}), catdir($class->dirsFor('Bin'), $^O), $class->dirsFor('Bin') ); + my @paths = ( catdir($baseDir, $class->{osDetails}->{'binArch'}), catdir($baseDir, $^O), $baseDir ); # Linux x86_64 should check its native folder first if ( $binArch =~ s/^x86_64-([^-]+).*/x86_64-$1/ ) { - unshift @paths, catdir($class->dirsFor('Bin'), $binArch); + unshift @paths, catdir($baseDir, $binArch); } elsif ( $class->{osDetails}->{'binArch'} eq 'armhf-linux' ) { - push @paths, catdir($class->dirsFor('Bin'), 'arm-linux'); + push @paths, catdir($baseDir, 'arm-linux'); + } + elsif ( $class->{osDetails}->{'binArch'} =~ /darwin/i && ($class->{'osDetails'}->{'osArch'} =~ /x86_64/ || $class->{'osDetails'}->{'osName'} =~ /\b10\.[1-9][4-9]\./) ) { + unshift @paths, catdir($baseDir, $class->{osDetails}->{'binArch'} . '-x86_64'), catdir($baseDir, $^O . '-x86_64'); } - + Slim::Utils::Misc::addFindBinPaths(@paths); # add path to Extension installer loaded plugins to @INC, NB this can only be done here as it requires Prefs to be loaded @@ -120,9 +139,9 @@ MySQL server instead of the instance installed with SC sub initMySQL { my ($class, $dbclass) = @_; - + require File::Which; - + # try to figure out whether we have a locally running MySQL # which we can connect to using a socket file my $mysql_config = File::Which::which('mysql_config'); @@ -137,7 +156,7 @@ sub initMySQL { if ($socket && -S $socket) { $dbclass->socketFile($socket); } - + } } @@ -155,7 +174,7 @@ sub dirsFor { my $dir = shift; my @dirs = (); - + if ($dir eq "Plugins") { push @dirs, catdir($Bin, 'Slim', 'Plugin'); @@ -170,19 +189,19 @@ sub dirsFor { eval { $updateDir = catdir( Slim::Utils::Prefs::preferences('server')->get('cachedir'), $dir ); }; - + if ($@) { eval { $updateDir = catdir( Slim::Utils::Light::getPref('cachedir'), $dir ); }; } - + return unless $updateDir; - + mkdir $updateDir unless -d $updateDir; push @dirs, $updateDir; } - + return wantarray() ? @dirs : $dirs[0]; } @@ -204,20 +223,20 @@ sub logRotate { while ( defined (my $file = readdir(DIR)) ) { next if $file !~ /\.log$/i; - + $file = catdir($dir, $file); # max. log size (default: 100MB) if (-s $file > $maxSize) { - # keep one old copy + # keep one old copy my $oldfile = "$file.0"; unlink $oldfile if -e $oldfile; - + File::Copy::move($file, $oldfile); } } - + closedir(DIR); } @@ -231,12 +250,12 @@ we might need to encode the path to correctly handle non-latin characters. sub decodeExternalHelperPath { my $path = $_[1]; - + # Bug 8118, only decode if filename can't be found # No. We need to set the UFT8 flag if we have non-ASCII contents - + $path = Slim::Utils::Unicode::utf8decode_locale($path); - + return $path; } @@ -274,6 +293,11 @@ sub getProxy { return $proxy; } +=head2 getDefaultGateway() + Get the network's default gateway address +=cut +sub getDefaultGateway { '' } + sub ignoredItems { return ( # Items we should ignore on a linux volume @@ -289,7 +313,7 @@ Get details about the locale, system language etc. sub localeDetails { require POSIX; - + my $lc_time = POSIX::setlocale(POSIX::LC_TIME()) || 'C'; my $lc_ctype = POSIX::setlocale(POSIX::LC_CTYPE()) || 'C'; @@ -325,7 +349,7 @@ sub localeDetails { $lc_ctype =~ s/euckr/euc-kr/i; $lc_ctype =~ s/big5/big5-eten/i; $lc_ctype =~ s/gb2312/euc-cn/i; - + return ($lc_ctype, $lc_time); } @@ -344,11 +368,13 @@ sub getSystemLanguage { sub _parseLanguage { my ($class, $language) = @_; - + $language = uc($language); $language =~ s/\.UTF.*$//; $language =~ s/(?:_|-|\.)\w+$//; - + + return 'EN' if $language && $language eq 'C'; + return $language || 'EN'; } @@ -360,7 +386,7 @@ Get a list of values from the osDetails list sub get { my $class = shift; - + if ( wantarray ) { return map { $class->{osDetails}->{$_} } grep { $class->{osDetails}->{$_} } @_; @@ -412,7 +438,7 @@ sub getPriority { =head2 initUpdate( ) -Initialize download of a potential updated Logitech Media Server version. +Initialize download of a potential updated Logitech Media Server version. Not needed on Linux distributions which do manage the update through their repositories. =cut @@ -423,19 +449,28 @@ sub canAutoUpdate { 0 }; sub installerExtension { '' }; sub installerOS { '' }; -# XXX - disable AutoRescan for all but SqueezeOS for now sub canAutoRescan { 0 } # can we use more memory to improve DB performance? sub canDBHighMem { 0 } +sub canVacuumInMemory { + my ($class, $dbSize) = @_; + + return unless Slim::Utils::Prefs::preferences('server')->get('dbhighmem'); + + # only vacuum in memory if the file is smaller than 512MB + # this should be ok for LMS on Windows/macOS, where we usually have plenty of RAM + return $dbSize < 512 * 1024 * 1024; +} + # some systems support checking ACLs in addition to simpler file tests my $filetest; sub aclFiletest { my ($class, $cb) = @_; - + $filetest = $cb if $cb; - + return $filetest; } diff --git a/Slim/Utils/OS/Debian.pm b/Slim/Utils/OS/Debian.pm index 70c5163691f..dd86fbacfaf 100644 --- a/Slim/Utils/OS/Debian.pm +++ b/Slim/Utils/OS/Debian.pm @@ -1,6 +1,6 @@ package Slim::Utils::OS::Debian; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/OS/Linux.pm b/Slim/Utils/OS/Linux.pm index 61e85f4cf97..934c73a8830 100644 --- a/Slim/Utils/OS/Linux.pm +++ b/Slim/Utils/OS/Linux.pm @@ -1,8 +1,8 @@ package Slim::Utils::OS::Linux; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -24,43 +24,80 @@ sub initDetails { sub canDBHighMem { my $class = shift; - - require File::Slurp; - - if ( my $meminfo = File::Slurp::read_file('/proc/meminfo') ) { - if ( $meminfo =~ /MemTotal:\s+(\d+) (\S+)/sig ) { - my ($value, $unit) = ($1, $2); - - # some 1GB systems grab RAM for the video adapter - enable dbhighmem if > 900MB installed - if ( ($unit =~ /KB/i && $value > 900_000) || ($unit =~ /MB/i && $value > 900) ) { - return 1; - } - } + + my $meminfo = getMemInfo(); + + if ($meminfo && $meminfo->{MemTotal}) { + # some 1GB systems grab RAM for the video adapter - enable dbhighmem if > 900MB installed + return $meminfo->{MemTotal} > 900_000_000; } - + # in case we haven't been able to read /proc/meminfo, enable dbhighmem for x86 systems return $class->{osDetails}->{'osArch'} =~ /[x6]86/ ? 1 : 0; } +sub canVacuumInMemory { + my ($class, $dbSize) = @_; + + return unless Slim::Utils::Prefs::preferences('server')->get('dbhighmem'); + + my $meminfo = getMemInfo() || return; + + # we're good if we have two times the library file's size in memory available + return ($meminfo->{MemAvailable} + $meminfo->{SwapFree}) > (2 * $dbSize); +} + +sub getMemInfo() { + open(INFIL,"/proc/meminfo") || return; + + my %units = ( + kb => 1024, + mb => 1024 * 1024, + gb => 1024 * 1024 * 1024 + ); + + my %mem; + foreach() { + if ( m/^(\S+):\s+(\S+) (\S+)/ ) { + $mem{$1} = $2 * ($units{lc($3)} || 0); + } + } + + $mem{MemAvailable} ||= $mem{MemFree} + $mem{Buffers}; + + close(INFIL); + return \%mem +} + sub getFlavor { + my $osName = ''; + + # parse new-school operating system identification file if available + if (-f '/etc/os-release' && open(OS_RELEASE, '/etc/os-release')) { + while () { + if (/^NAME="(.*?)"/i) { + $osName = lc($1); + last; + } + } + + close OS_RELEASE; + } + if (-f '/etc/raidiator_version') { return 'Netgear RAIDiator'; - - } elsif (-f '/etc/squeezeos.version') { - - return 'SqueezeOS'; - - } elsif (-f '/etc/debian_version') { - + + } elsif ($osName =~ /debian|devuan|ubuntu|raspbian/ || -f '/etc/debian_version' || -f '/etc/devuan_version') { + return 'Debian'; - - } elsif (-f '/etc/redhat_release' || -f '/etc/redhat-release' || -f '/etc/fedora-release') { - + + } elsif ($osName =~ /red.?hat|fedora|centos/ || -f '/etc/redhat_release' || -f '/etc/redhat-release' || -f '/etc/fedora-release') { + return 'Red Hat'; - - } elsif (-f '/etc/SuSE-release') { - + + } elsif ($osName =~ /suse|sles/ || -f '/etc/SuSE-release') { + return 'SuSE'; } elsif (-f '/etc/synoinfo.conf' || -f '/etc.defaults/synoinfo.conf') { @@ -73,11 +110,23 @@ sub getFlavor { sub signalUpdateReady { my ($file) = @_; - + if ($file) { - $file =~ /(\d\.\d\.\d).*?(\d{5,})/; - $::newVersion = Slim::Utils::Strings::string('SERVER_LINUX_UPDATE_AVAILABLE', "$1 - $2", $file); + my ($version, $revision) = $file =~ /(\d+\.\d+\.\d+)(?:.*(\d{5,}))?/; + $revision ||= 0; + $::newVersion = Slim::Utils::Strings::string('SERVER_LINUX_UPDATE_AVAILABLE', "$version - $revision", $file); + } +} + +sub getDefaultGateway { + my $route = `/sbin/route -n`; + while ( $route =~ /^(?:0\.0\.0\.0)\s*(\d+\.\d+\.\d+\.\d+)/mg ) { + if ( Slim::Utils::Network::ip_is_private($1) ) { + return $1; + } } + + return; } 1; diff --git a/Slim/Utils/OS/OSX.pm b/Slim/Utils/OS/OSX.pm index 2edec9bc0b0..5695eb42cd9 100644 --- a/Slim/Utils/OS/OSX.pm +++ b/Slim/Utils/OS/OSX.pm @@ -1,8 +1,8 @@ package Slim::Utils::OS::OSX; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -22,10 +22,10 @@ sub name { sub initDetails { my $class = shift; - + if ( !main::RESIZER ) { # Once for OS Version, then again for CPU Type. - open(SYS, '/usr/sbin/system_profiler SPSoftwareDataType SPHardwareDataType |') or return; + open(SYS, '/usr/sbin/system_profiler SPSoftwareDataType SPHardwareDataType 2>&1 |') or return; while () { @@ -33,14 +33,14 @@ sub initDetails { $class->{osDetails}->{'osName'} = $1; $class->{osDetails}->{'osName'} =~ s/ \(\w+?\)$//; - + } elsif (/Intel/i) { # Determine if we are running as 32-bit or 64-bit my $bits = length( pack 'L!', 1 ) == 8 ? 64 : 32; - + $class->{osDetails}->{'osArch'} = 'x86'; - + if ( $bits == 64 ) { $class->{osDetails}->{'osArch'} = 'x86_64'; } @@ -49,20 +49,32 @@ sub initDetails { $class->{osDetails}->{'osArch'} = 'ppc'; } - + last if $class->{osDetails}->{'osName'} && $class->{osDetails}->{'osArch'}; } close SYS; } + if ( !$class->{osDetails}->{osArch} ) { + if ($Config{ccflags} =~ /-arch x86_64/) { + $class->{osDetails}->{osArch} = 'x86_64'; + } + elsif ($Config{ccflags} =~ /-arch i386/) { + $class->{osDetails}->{osArch} = 'x86'; + } + elsif ($Config{ccflags} =~ /-arch ppc/) { + $class->{osDetails}->{osArch} = 'ppc'; + } + } + $class->{osDetails}->{'os'} = 'Darwin'; $class->{osDetails}->{'uid'} = getpwuid($>); # XXX - do we still need this? They're empty on my system, and created if needed in some other place anyway for my $dir ( 'Library/Application Support/Squeezebox', - 'Library/Application Support/Squeezebox/Plugins', + 'Library/Application Support/Squeezebox/Plugins', 'Library/Application Support/Squeezebox/Graphics', 'Library/Application Support/Squeezebox/html', 'Library/Application Support/Squeezebox/IR', @@ -74,35 +86,35 @@ sub initDetails { unshift @INC, $ENV{'HOME'} . "/Library/Application Support/Squeezebox"; unshift @INC, "/Library/Application Support/Squeezebox"; - + return $class->{osDetails}; } sub initPrefs { my ($class, $prefs) = @_; - + $prefs->{libraryname} = `scutil --get ComputerName` || ''; chomp($prefs->{libraryname}); # Replace fancy apostraphe (’) with ASCII utf8::decode( $prefs->{libraryname} ) unless utf8::is_utf8($prefs->{libraryname}); $prefs->{libraryname} =~ s/\x{2019}/'/; - + # we now have a binary preference pane - don't show the wizard $prefs->{wizardDone} = 1; } sub canDBHighMem { 1 } -sub canFollowAlias { +sub canFollowAlias { return $canFollowAlias if defined $canFollowAlias; - + eval { require Mac::Files; require Mac::Resources; $canFollowAlias = 1; }; - + if ( $@ ) { $canFollowAlias = 0; } @@ -110,14 +122,14 @@ sub canFollowAlias { sub initSearchPath { my $class = shift; - - $class->SUPER::initSearchPath(); + + $class->SUPER::initSearchPath(@_); my @paths = (); push @paths, $ENV{'HOME'} ."/Library/iTunes/Scripts/iTunes-LAME.app/Contents/Resources/"; push @paths, (split(/:/, $ENV{'PATH'}), qw(/usr/bin /usr/local/bin /usr/libexec /sw/bin /usr/sbin /opt/local/bin)); - + Slim::Utils::Misc::addFindBinPaths(@paths); } @@ -134,7 +146,7 @@ sub dirsFor { my ($class, $dir) = @_; my @dirs = $class->SUPER::dirsFor($dir); - + # These are all at the top level. if ($dir =~ /^(?:strings|revision|convert|types|repositories)$/) { @@ -167,8 +179,8 @@ sub dirsFor { if ($::prefsfile && -r $::prefsfile) { push @dirs, $::prefsfile; - } - + } + elsif (-r catdir($ENV{'HOME'}, 'Library', 'SlimDevices', 'slimserver.pref')) { push @dirs, catdir($ENV{'HOME'}, 'Library', 'SlimDevices', 'slimserver.pref'); @@ -177,11 +189,11 @@ sub dirsFor { } elsif ($dir eq 'prefs') { push @dirs, $::prefsdir || catdir($ENV{'HOME'}, '/Library/Application Support/Squeezebox'); - + } elsif ($dir =~ /^(?:music|videos|pictures)$/) { my $mediaDir; - + if ($dir eq 'music') { # DHG wants LMS to default to the full Music folder, not only iTunes # $mediaDir = catdir($ENV{'HOME'}, 'Music', 'iTunes'); @@ -204,7 +216,7 @@ sub dirsFor { push @dirs, $mediaDir; } elsif ($dir eq 'playlists') { - + push @dirs, catdir($class->dirsFor('music'), 'Playlists'); # We might get called from some helper script (update checker) @@ -245,7 +257,7 @@ sub localeDetails { # language / formatting. Set it here, so we don't need to do a # system call for every clock second update. my $lc_time = POSIX::setlocale(LC_TIME, $locale); - + return ($lc_ctype, $lc_time); } @@ -268,7 +280,7 @@ sub getSystemLanguage { close(LANG); } - + return $class->_parseLanguage($language); } @@ -299,7 +311,7 @@ sub ignoredItems { 'tmp' => 1, 'usr' => 1, 'var' => '/', - 'opt' => '/', + 'opt' => '/', ); } @@ -319,43 +331,54 @@ INIT { sub pathFromMacAlias { my ($class, $fullpath) = @_; - + return unless $fullpath && canFollowAlias(); - + my $path; - + $fullpath = Slim::Utils::Misc::pathFromFileURL($fullpath) unless $fullpath =~ m|^/|; - + if ( exists $aliases{$fullpath} ) { return $aliases{$fullpath}; } if (-f $fullpath && -r _ && (my $rsc = Mac::Resources::FSpOpenResFile($fullpath, 0))) { - + if (my $alis = Mac::Resources::GetIndResource('alis', 1)) { - + $path = $aliases{$fullpath} = Mac::Files::ResolveAlias($alis); - + Mac::Resources::ReleaseResource($alis); } - + Mac::Resources::CloseResFile($rsc); } return $path; } +sub getDefaultGateway { + my $route = `route -n get default`; + if ($route =~ /gateway:\s*(\d+\.\d+\.\d+\.\d+)/s ) { + if ( Slim::Utils::Network::ip_is_private($1) ) { + return $1; + } + } + + return; +} + my $updateCheckInitialized; my $plistLabel = "com.slimdevices.updatecheck"; sub initUpdate { return if $updateCheckInitialized; - + my $log = Slim::Utils::Log::logger('server.update'); my $err = "Failed to install LaunchAgent for the update checker"; - + my $launcherPlist = catfile($ENV{HOME}, 'Library', 'LaunchAgents', $plistLabel . '.plist'); - + if ( open(UPDATE_CHECKER, ">$launcherPlist") ) { my $script = Slim::Utils::Misc::findbin('check-update.pl'); my $logDir = Slim::Utils::Log::serverLogFile(); @@ -391,10 +414,10 @@ sub initUpdate { ); close UPDATE_CHECKER; - + $err = `launchctl unload $launcherPlist; launchctl load $launcherPlist`; } - + if ($err) { $log->error($err); } @@ -423,7 +446,7 @@ sub getUpdateParams { sub canAutoUpdate { 1 } -sub installerExtension { 'pkg' }; +sub installerExtension { 'pkg' }; sub installerOS { 'osx' } sub canRestartServer { diff --git a/Slim/Utils/OS/ReadyNAS.pm b/Slim/Utils/OS/ReadyNAS.pm index df651c645e2..ef7242f6d8c 100644 --- a/Slim/Utils/OS/ReadyNAS.pm +++ b/Slim/Utils/OS/ReadyNAS.pm @@ -1,6 +1,6 @@ package Slim::Utils::OS::ReadyNAS; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -141,8 +141,9 @@ sub getUpdateParams { my ($class, $url) = @_; if ($url) { - $url =~ /(\d\.\d\.\d).*?(\d{5,})/; - $::newVersion = Slim::Utils::Strings::string('SERVER_UPDATE_AVAILABLE', "$1 - $2", $url); + my ($version, $revision) = $url =~ /(\d+\.\d+\.\d+)(?:.*(\d{5,}))?/; + $revision ||= 0; + $::newVersion = Slim::Utils::Strings::string('SERVER_UPDATE_AVAILABLE', "$version - $revision", $url); } return; diff --git a/Slim/Utils/OS/RedHat.pm b/Slim/Utils/OS/RedHat.pm index f41d0c2ff14..2ee176a6e40 100644 --- a/Slim/Utils/OS/RedHat.pm +++ b/Slim/Utils/OS/RedHat.pm @@ -1,6 +1,6 @@ package Slim::Utils::OS::RedHat; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/OS/SqueezeOS.pm b/Slim/Utils/OS/SqueezeOS.pm deleted file mode 100644 index cb37d4b38cd..00000000000 --- a/Slim/Utils/OS/SqueezeOS.pm +++ /dev/null @@ -1,473 +0,0 @@ -package Slim::Utils::OS::SqueezeOS; - -use strict; -use base qw(Slim::Utils::OS::Linux); - -use constant SQUEEZEPLAY_PREFS => '/etc/squeezeplay/userpath/settings/'; -use constant SP_PREFS_JSON => '/etc/squeezecenter/prefs.json'; -use constant SP_SCAN_JSON => '/etc/squeezecenter/scan.json'; - -# Cannot use Slim::Utils::Prefs here because too early in bootstrap process. - -sub dontSetUserAndGroup { 1 } - -sub initDetails { - my $class = shift; - - $class->{osDetails} = $class->SUPER::initDetails(); - - # package specific addition to @INC to cater for plugin locations - $class->{osDetails}->{isSqueezeOS} = 1; - - if ( !main::SCANNER && -r '/proc/cpuinfo' ) { - # Read UUID from cpuinfo - open my $fh, '<', '/proc/cpuinfo' or die "Unable to read /proc/cpuinfo: $!"; - while ( <$fh> ) { - if ( /^UUID\s+:\s+([0-9a-f-]+)/ ) { - my $uuid = $1; - $uuid =~ s/-//g; - $class->{osDetails}->{uuid} = $uuid; - } - } - close $fh; - } - - if ( !main::SCANNER && -r '/sys/class/net/eth0/address' ) { - # Read MAC - open my $fh, '<', '/sys/class/net/eth0/address' or die "Unable to read /sys/class/net/eth0/address: $!"; - while ( <$fh> ) { - if ( /^([0-9a-f]{2}([:]|$)){6}$/i ) { - $class->{osDetails}->{mac} = $_; - chomp $class->{osDetails}->{mac}; - last; - } - } - close $fh; - } - - return $class->{osDetails}; -} - -sub ignoredItems { - my $class = shift; - - my %ignoredItems = $class->SUPER::ignoredItems(); - - # ignore some Windows special folders which exist on external disks too - # can't ignore Recycler though... http://www.lastfm.de/music/Recycler - $ignoredItems{'System Volume Information'} = 1; - $ignoredItems{'RECYCLER'} = 1; - $ignoredItems{'$Recycle.Bin'} = 1; - $ignoredItems{'$RECYCLE.BIN'} = 1; - $ignoredItems{'log'} = 1; - - return %ignoredItems; -} - -sub initPrefs { - my ($class, $defaults) = @_; - - $defaults->{maxPlaylistLength} = 100; - $defaults->{libraryname} = "Squeezebox Touch"; - $defaults->{autorescan} = 1; - $defaults->{disabledextensionsvideo} = 'VIDEO'; # don't scan videos on SqueezeOS - $defaults->{disabledextensionsimages} = 'bmp, gif, png';# scaling down non-jpg might use too much memory - $defaults->{dontTriggerScanOnPrefChange} = 0; -} - -sub canDBHighMem { 0 } - -my %prefSyncHandlers = ( - SQUEEZEPLAY_PREFS . 'SetupLanguage.lua' => sub { - my $data = shift; - - if ($$data && $$data =~ /locale="([A-Z][A-Z])"/) { - Slim::Utils::Prefs::preferences('server')->set('language', uc($1)); - } - }, - - SQUEEZEPLAY_PREFS . 'SetupDateTime.lua' => sub { - my $data = shift; - - if ($$data) { - my $prefs = Slim::Utils::Prefs::preferences('server'); - - if ($$data =~ /dateformat="(.*?)"/) { - $prefs->set('longdateFormat', $1); - } - - if ($$data =~ /shortdateformat="(.*?)"/) { - $prefs->set('shortdateFormat', $1); - } - - # Squeezeplay only knows 12 vs. 24h time, but no fancy formats as Logitech Media Server - $prefs->set('timeFormat', $$data =~ /hours="24"/ ? '%H:%M' : '|%I:%M %p'); - - $prefs = Slim::Utils::Prefs::preferences('plugin.datetime'); - foreach ( Slim::Player::Client::clients() ) { - $prefs->client($_)->set('dateFormat', ''); - $prefs->client($_)->set('timeFormat', ''); - } - } - }, - - SQUEEZEPLAY_PREFS . 'Playback.lua' => sub { - my $data = shift; - - if ($$data && $$data =~ /playerInit={([^}]+)}/i) { - my $playerInit = $1; - - if ($playerInit =~ /name="(.*?)"/i) { - my $prefs = Slim::Utils::Prefs::preferences('server'); - $prefs->set('libraryname', $1); - - # can't handle this change using a changehandler, - # as this in turn updates the pref again - _updateLibraryname($prefs); - } - } - }, -) unless main::SCANNER; - -my ($i, $w); -sub postInitPrefs { - my ( $class, $prefs ) = @_; - - _checkMediaAtStartup($prefs); - - $prefs->setChange( \&_onAudiodirChange, 'mediadirs', 'FIRST' ); - $prefs->setChange( sub { - _updateLibraryname($prefs); - }, 'language', 'mediadirs' ); - $prefs->setChange( \&_onSNTimediffChange, 'sn_timediff'); - - if ( !main::SCANNER ) { - - # sync up prefs in case they were changed while the server wasn't running - foreach (keys %prefSyncHandlers) { - _syncPrefs($_); - } - - # initialize prefs syncing between Squeezeplay and the server - eval { - require Linux::Inotify2; - import Linux::Inotify2; - - $i = Linux::Inotify2->new() or die "Unable to start Inotify watcher: $!"; - - $i->watch(SQUEEZEPLAY_PREFS, Linux::Inotify2::IN_MOVE() | Linux::Inotify2::IN_MODIFY(), sub { - my $ev = shift; - my $file = $ev->fullname || ''; - - # $ev->fullname sometimes adds duplicate slashes - $file =~ s|//|/|g; - - _syncPrefs($file); - - }) or die "Unable to add Inotify watcher: $!"; - - $w = AnyEvent->io( - fh => $i->fileno, - poll => 'r', - cb => sub { $i->poll }, - ); - }; - - Slim::Utils::Log::logError("Squeezeplay <-> Server prefs syncing failed to initialize: $@") if ($@); - } -} - -# add media name to the libraryname -sub _updateLibraryname { - require Slim::Utils::Strings; - - my $prefs = $_[0]; - my $libraryname = $prefs->get('libraryname'); - - # remove media name - $libraryname =~ s/ \(.*?(?:USB|SD).*?\)$//i; - - # XXX - for the time being we're going to assume that the embedded server will only handle one folder - my $audiodir = Slim::Utils::Misc::getAudioDirs()->[0]; - if ( $audiodir && $audiodir =~ m{/(mmcblk|sd[a-z]\d)}i ) { - $libraryname = sprintf( "%s (%s)", $libraryname, Slim::Utils::Strings::getString($1 =~ /mmc/ ? 'SD' : 'USB') ); - } - - $prefs->set('libraryname', $libraryname); -} - -sub _syncPrefs { - my $file = shift; - - if ($file && $prefSyncHandlers{$file} && -r $file ) { - - require File::Slurp; - - my $data = File::Slurp::read_file($file); - - # bug 15882 - SP writes files with UTF-8 encoding - utf8::decode($data); - - &{ $prefSyncHandlers{$file} }(\$data); - } -} - - -sub sqlHelperClass { 'Slim::Utils::SQLiteHelper' } - -=head2 dirsFor( $dir ) - -Return OS Specific directories. - -Argument $dir is a string to indicate which of the server directories we -need information for. - -=cut - -sub dirsFor { - my ($class, $dir) = @_; - - my @dirs = (); - - if ($dir =~ /^(?:scprefs|oldprefs|updates)$/) { - - push @dirs, $class->SUPER::dirsFor($dir); - - } elsif ($dir =~ /^(?:Firmware|Graphics|HTML|IR|SQLite|SQL|lib|Bin)$/) { - - push @dirs, "/usr/squeezecenter/$dir"; - - } elsif ($dir eq 'Plugins') { - - push @dirs, $class->SUPER::dirsFor($dir); - push @dirs, "/usr/squeezecenter/Slim/Plugin", "/usr/share/squeezecenter/Plugins"; - - } elsif ($dir =~ /^(?:strings|revision|repositories)$/) { - - push @dirs, "/usr/squeezecenter"; - - } elsif ($dir eq 'libpath') { - - push @dirs, "/usr/squeezecenter"; - - } elsif ($dir =~ /^(?:types|convert)$/) { - - push @dirs, "/usr/squeezecenter"; - - } elsif ($dir =~ /^(?:prefs)$/) { - - push @dirs, $::prefsdir || "/etc/squeezecenter/prefs"; - - } elsif ($dir eq 'log') { - - push @dirs, $::logdir || "/var/log/squeezecenter"; - - } elsif ($dir eq 'cache') { - - # XXX: cachedir pref is going to cause a problem here, we need to ignore it - push @dirs, $::cachedir || "/etc/squeezecenter/cache"; - - } elsif ($dir =~ /^(?:music)$/) { - - push @dirs, ''; - - } elsif ($dir =~ /^(?:playlists)$/) { - - push @dirs, ''; - - } else { - - warn "dirsFor: Didn't find a match request: [$dir]\n"; - } - - return wantarray() ? @dirs : $dirs[0]; -} - -# Bug 9488, always decode on Ubuntu/Debian -sub decodeExternalHelperPath { - return Slim::Utils::Unicode::utf8decode_locale($_[1]); -} - -sub scanner { - return '/usr/squeezecenter/scanner.pl'; -} - -sub gdresize { - return '/usr/squeezecenter/gdresize.pl'; -} - -sub gdresized { - return '/usr/squeezecenter/gdresized.pl'; -} - -# See corresponding list in SqueezeOS SbS build file: squeezecenter_svn.bb -# Only file listed there in INCLUDED_PLUGINS are actually installed -sub skipPlugins { - my $class = shift; - - return ( - qw( - RSSNews Podcast InfoBrowser - RS232 Visualizer SlimTris Snow NetTest - - Extensions JiveExtras - - iTunes MusicMagic PreventStandby Rescan TT xPL - - UPnP ImageBrowser - - ACLFiletest - DnDPlay ExtendedBrowseModes FullTextSearch LibraryDemo - ), - $class->SUPER::skipPlugins(), - ); -} - -sub _setupMediaDir { - my ( $path, $prefs ) = @_; - - # Is audiodir defined, mounted and writable? - if ($path && $path =~ m{^/media/[^/]+} && -w $path) { - - require File::Slurp; - - my $mounts = grep /$path/, split /\n/, File::Slurp::read_file('/proc/mounts'); - - if ( !$mounts ) { - warn "$path: mount point is not mounted, let's ignore it\n"; - return 0; - } - - # XXX Maybe also check for rw mount-point - - # Create a .Squeezebox directory if necessary - if ( !-e "$path/.Squeezebox" ) { - mkdir "$path/.Squeezebox" or do { - warn "Unable to create directory $path/.Squeezebox: $!\n"; - return 0; - }; - } - - if ( !-e "$path/.Squeezebox/cache" ) { - mkdir "$path/.Squeezebox/cache" or do { - warn "Unable to create directory $path/.Squeezebox/cache: $!\n"; - return 0; - }; - } - - $prefs->set( mediadirs => [ $path ] ); - $prefs->set( librarycachedir => "$path/.Squeezebox/cache" ); - - # reset dbsource, it needs to be re-configured - $prefs->set( 'dbsource', '' ); - - # Create a playlist dir if necessary - my $playlistdir = "$path/Playlists"; - - if ( -f $playlistdir ) { - $playlistdir .= 'Squeezebox'; - } - - if ( !-d $playlistdir ) { - mkdir $playlistdir or warn "Couldn't create playlist directory: $playlistdir - $!\n"; - } - - $prefs->set( playlistdir => $playlistdir ); - - return 1; - } - - return 0; -} - -sub _onAudiodirChange { - require Slim::Utils::Prefs; - my $prefs = Slim::Utils::Prefs::preferences('server'); - - # XXX - for the time being we're going to assume that the embedded server will only handle one folder - my $audiodir = Slim::Utils::Misc::getAudioDirs()->[0]; - - if ($audiodir) { - if (_setupMediaDir($audiodir, $prefs)) { - # hunky dory - return; - } else { - # it is defined but not valid - warn "$audiodir cannot be used"; - $prefs->set('mediadirs', []); - } - } -} - -sub _checkMediaAtStartup { - my $prefs = shift; - - # Always read audiodir first from /etc/squeezecenter/prefs.json - my $audiodir = ''; - if ( -e SP_PREFS_JSON ) { - require File::Slurp; - require JSON::XS; - - my $spPrefs = eval { JSON::XS::decode_json( File::Slurp::read_file(SP_PREFS_JSON) ) }; - if ( $@ ) { - warn "Unable to read prefs.json: $@\n"; - } - else { - $audiodir = $spPrefs->{audiodir}; - } - } - - # XXX SP should store audiodir and mountpath values - # mediapath is always the root of the drive and where we store the .Squeezebox dir - # audiodir may be any other dir, but .Squeezebox dir is not put there - - if ( _setupMediaDir($audiodir, $prefs) ) { - # hunky dory - return; - } - - # Something went wrong, don't use this audiodir - $prefs->set('mediadirs', []); -} - -# Update system time if difference between system and SN time is bigger than 15 seconds -sub _onSNTimediffChange { - my $pref = shift; - my $diff = shift; - - if( abs( $diff) > 15) { - Slim::Utils::OS::SqueezeOS->settimeofday( time() + $diff); - } -} - -# don't download/cache firmware for other players, but have them download directly -sub directFirmwareDownload { 1 }; - -# Path to progress JSON file -sub progressJSON { SP_SCAN_JSON } - -# This is a bit of a hack to call the settimeofday() syscall to set the system time -# without the need to shell out to another process. -sub settimeofday { - my ( $class, $epoch ) = @_; - - eval { - # int settimeofday(const struct timeval *tv , const struct timezone *tz); - # struct timeval is long, long - # struct timezone is int, int, but ignored - my $ret = syscall( 79, pack('LLLL', $epoch, 0, 0, 0), 0 ); - if ( $ret != 0 ) { - die $!; - } - }; - - if ( $@ ) { - Slim::Utils::Log::logWarning("settimeofday($epoch) failed: $@"); - } else { - Slim::Utils::Timers::timeChanged(); - } -} - -sub canAutoRescan { 1 }; - -1; diff --git a/Slim/Utils/OS/Suse.pm b/Slim/Utils/OS/Suse.pm index 93ee8764ee1..99a0d9b363a 100644 --- a/Slim/Utils/OS/Suse.pm +++ b/Slim/Utils/OS/Suse.pm @@ -1,6 +1,6 @@ package Slim::Utils::OS::Suse; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/OS/Synology.pm b/Slim/Utils/OS/Synology.pm index 1ad4c9a80bc..4e55f9cea29 100644 --- a/Slim/Utils/OS/Synology.pm +++ b/Slim/Utils/OS/Synology.pm @@ -1,61 +1,44 @@ package Slim::Utils::OS::Synology; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. # -# This module written by Philippe Kehl -# -# Synology DiskStation (DS) include a wide range of NAS devices based on -# several architectures (PPC, ARM, PPCe500v2, ARMv5, and maybe others). They -# all use a custom and minimal Linux system (Linux 2.6 based). There are (to my -# knowledge) three options to run Logitech Media Server on these devices: -# -# 1) flipflip's SlimServer on DiskStation (SSODS) provides a system -# (libc, Perl, tools etc.) spcifically to run Logitech Media Server. -# 2) Synology recently added Perl to its standard system and provides an -# add-on Logitech Media Server package (via the DSM Package Management). -# 3) "Optware", a package for feed for numerous add-on packages from the -# NSLU2-Linux project, provides a Logitech Media Server package and its dependencies. -# -# This module is trying to provide customisations for all these options. +# This module was initially written by Philippe Kehl use strict; -use File::Spec::Functions qw(:ALL); -use FindBin qw($Bin); + +use Config; use base qw(Slim::Utils::OS::Linux); use constant MAX_LOGSIZE => 1024*1024*1; # maximum log size: 1 MB +use constant MUSIC_DIR => '/volume1/music'; +use constant PHOTOS_DIR => '/volume1/photo'; +use constant VIDEOS_DIR => '/volume1/video'; - -sub initDetails -{ +sub initDetails { my $class = shift; $class->{osDetails} = $class->SUPER::initDetails(); + $class->{osDetails}->{osArch} ||= $Config{'archname'}; $class->{osDetails}->{isDiskStation} = 1; - # check how this Logitech Media Server is run on the DiskStation - if (-f '/volume1/SSODS/etc/ssods/ssods.conf' - && "@INC" =~ m{/volume1/SSODS/lib/perl}) - { - $class->{osDetails}->{isSSODS} = 1; - $class->{osDetails}->{osName} .= ' (SSODS)'; - } - elsif (-d '/opt/share/squeezecenter' - && "@INC" =~ m{/opt/lib/perl}) - { - $class->{osDetails}->{isOptware} = 1; - $class->{osDetails}->{osName} .= ' (NSLU2-Linux Optware)'; - } - elsif (-d '/volume1/@appstore/SqueezeCenter' - && "@INC" =~ m{/usr/lib/perl}) - { - $class->{osDetails}->{isSynology} = 1; - $class->{osDetails}->{osName} .= ' (DSM Package Management)'; + if ( !main::RESIZER && !main::SCANNER ) { + open(my $versionInfo, '<', '/etc/VERSION') or warn "Can't open /etc/VERSION: $!"; + + if ($versionInfo) { + while (<$versionInfo>) { + if (/productversion="(.*?)"/i) { + $class->{osDetails}->{osName} = "Synology DSM $1"; + last; + } + } + + close $versionInfo; + }; } return $class->{osDetails}; @@ -69,50 +52,108 @@ sub localeDetails { } -sub logRotate -{ +sub logRotate { my $class = shift; my $dir = shift || Slim::Utils::OSDetect::dirsFor('log'); - # only keep small log files (1MB) because they are displayed - # (if at all) in a web interface - Slim::Utils::OS->logRotate($dir, MAX_LOGSIZE); + # only keep small log files (1MB) because they are displayed + # (if at all) in a web interface + Slim::Utils::OS->logRotate($dir, MAX_LOGSIZE); +} + +sub dirsFor { + my ($class, $dir) = @_; + + my @dirs = $class->SUPER::dirsFor($dir); + + if ($dir =~ /^(?:music|videos|pictures)$/) { + my $mediaDir; + + if ($dir eq 'music' && -d MUSIC_DIR) { + $mediaDir = MUSIC_DIR; + } + # elsif ($dir eq 'videos' && -d VIDEOS_DIR) { + # $mediaDir = VIDEOS_DIR; + # } + # elsif ($dir eq 'pictures' && -d PHOTOS_DIR) { + # $mediaDir = PHOTOS_DIR; + # } + + push @dirs, $mediaDir if $mediaDir; + } + + return wantarray() ? @dirs : $dirs[0]; } +# ignore the many @... sub-folders. The static list in ignoredItems() would never be complete. +sub postInitPrefs { + my ($class, $prefs) = @_; + + # only do this once - if somebody decides to modify the value, so be it! + if (!$prefs->get('ignoreDirREForSynologySet') && !$prefs->get('ignoreDirRE')) { + $prefs->set('ignoreDirRE', '^@[a-zA-Z]+'); + $prefs->set('ignoreDirREForSynologySet', 1); + } +} -sub ignoredItems -{ +sub ignoredItems { return ( - '@eaDir' => 1, # media indexer meta data - '@spool' => 1, # mail/print/.. spool - '@tmp' => 1, # system temporary files - '@appstore' => 1, # Synology package manager - '@database' => 1, # databases store - '@optware' => 1, # NSLU2-Linux Optware system - 'upd@te' => 1, # firmware update temporary directory - # system paths in the fs root which will not contain any music - 'bin' => '/', - 'dev' => '/', - 'etc' => '/', - 'etc.defaults' => '/', - 'home' => '/', - 'initrd' => '/', - 'lib' => '/', - 'linuxrc' => '/', - 'lost+found' => 1, - 'mnt' => '/', - 'opt' => '/', - 'proc' => '/', - 'root' => '/', - 'sbin' => '/', - 'sys' => '/', - 'tmp' => '/', - 'usr' => '/', - 'var' => '/', - 'var.defaults' => '/', - # now only the data partition mount points /volume(|USB)[0-9] - # should remain - ); + '@AntiVirus' => 1, + '@appstore' => 1, # Synology package manager + '@autoupdate' => 1, + '@clamav' => 1, + '@cloudsync' => 1, + '@database' => 1, # databases store + '@download' => 1, + '@eaDir' => 1, # media indexer meta data + '@img_bkp_cache' => 1, + '@maillog' => 1, + '@MailScanner' => 1, + '@optware' => 1, # NSLU2-Linux Optware system + '@postfix' => 1, + '@quarantine' => 1, + '@S2S' => 1, + '@sharesnap' => 1, + '@spool' => 1, # mail/print/.. spool + '@SynoFinder-log' => 1, + '@synodlvolumeche.core' => 1, + '@SynologyApplicationService' => 1, + '@synologydrive' => 1, + '@SynologyDriveShareSync' => 1, + '@synopkg' => 1, + '@synovideostation' => 1, + '@tmp' => 1, # system temporary files + 'upd@te' => 1, # firmware update temporary directory + '#recycle' => 1, + '#snapshot' => 1, + # system paths in the fs root which will not contain any music + 'bin' => '/', + 'config' => '/', + 'dev' => '/', + 'etc' => '/', + 'etc.defaults' => '/', + 'home' => '/', + 'initrd' => '/', + 'lib' => '/', + 'lib32' => '/', + 'lib64' => '/', + 'linuxrc' => '/', + 'lost+found' => 1, + 'mnt' => '/', + 'opt' => '/', + 'proc' => '/', + 'root' => '/', + 'run' => '/', + 'sbin' => '/', + 'sys' => '/', + 'tmp' => '/', + 'tmpRoot' => '/', + 'usr' => '/', + 'var' => '/', + 'var.defaults' => '/', + # now only the data partition mount points /volume(|USB)[0-9] + # should remain + ); } 1; diff --git a/Slim/Utils/OS/Unix.pm b/Slim/Utils/OS/Unix.pm index c5dd758d20d..3039d5adbea 100644 --- a/Slim/Utils/OS/Unix.pm +++ b/Slim/Utils/OS/Unix.pm @@ -1,6 +1,6 @@ package Slim::Utils::OS::Unix; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -30,7 +30,7 @@ sub initDetails { sub initSearchPath { my $class = shift; - $class->SUPER::initSearchPath(); + $class->SUPER::initSearchPath(@_); my @paths = (split(/:/, ($ENV{'PATH'} || '/sbin:/usr/sbin:/bin:/usr/bin')), qw(/usr/bin /usr/local/bin /usr/libexec /sw/bin /usr/sbin /opt/bin)); diff --git a/Slim/Utils/OS/Win32.pm b/Slim/Utils/OS/Win32.pm index 364a08cc88b..6497315ac2f 100644 --- a/Slim/Utils/OS/Win32.pm +++ b/Slim/Utils/OS/Win32.pm @@ -1,8 +1,8 @@ package Slim::Utils::OS::Win32; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -33,7 +33,7 @@ sub initDetails { # better version detection than relying on Win32::GetOSName() # http://msdn.microsoft.com/en-us/library/ms724429(VS.85).aspx my ($string, $major, $minor, $build, $id, $spmajor, $spminor, $suitemask, $producttype) = Win32::GetOSVersion(); - + $class->{osDetails} = { 'os' => 'Windows', 'osName' => (Win32::GetOSName())[0], @@ -46,7 +46,7 @@ sub initDetails { $class->{osDetails}->{'osName'} =~ s/Win/Windows /; $class->{osDetails}->{'osName'} =~ s/\/.Net//; $class->{osDetails}->{'osName'} =~ s/2003/Server 2003/; - + # TODO: remove this code as soon as Win32::GetOSName supports latest Windows versions # The version numbers for Windows 7 and Windows Server 2008 R2 are identical; the PRODUCTTYPE field must be used to differentiate between them. @@ -57,7 +57,7 @@ sub initDetails { # The version numbers for Windows 8 onwards are identical, Win32.pm has not been updated to cover these # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724832(v=vs.85).aspx elsif ($major == 6 && $minor == 2) { - + if ( my $wmi = Win32::OLE->GetObject( "WinMgmts://./root/cimv2" ) ) { if ( my $list = $wmi->InstancesOf( "Win32_OperatingSystem" ) ) { @@ -93,13 +93,13 @@ sub initDetails { $class->{osDetails}->{'osName'} = 'Windows Home Server'; $class->{osDetails}->{'isWHS'} = 1; } - + # give some fallback value $class->{osDetails}->{osName} ||= sprintf('Windows (%s, %s, %s)', $major, $minor, $producttype); - + # This covers Vista or later $class->{osDetails}->{'isWin6+'} = ($major >= 6); - + # some features are Vista only, no longer supported in Windows 7 $class->{osDetails}->{isVista} = 1 if $class->{osDetails}->{'osName'} =~ /Vista/; @@ -115,11 +115,11 @@ sub initDetails { sub initSearchPath { my $class = shift; - $class->SUPER::initSearchPath(); - + $class->SUPER::initSearchPath(@_); + # TODO: we might want to make this a bit more intelligent # as Perl is not always in that folder (eg. German Windows) - + Slim::Utils::Misc::addFindBinPaths('C:\Perl\bin'); } @@ -127,7 +127,7 @@ sub initMySQL {} sub initPrefs { my ($class, $prefs) = @_; - + # we now have a binary control panel - don't show the wizard $prefs->{wizardDone} = 1; } @@ -149,9 +149,9 @@ sub postInitPrefs { sub dirsFor { my ($class, $dir) = @_; - + my @dirs = $class->SUPER::dirsFor($dir); - + if ($dir =~ /^(?:strings|revision|convert|types|repositories)$/) { push @dirs, $Bin; @@ -169,15 +169,15 @@ sub dirsFor { if ($::prefsfile && -r $::prefsfile) { push @dirs, $::prefsfile; - } - + } + else { if ($class->{osDetails}->{'isWin6+'} && -r catdir($class->writablePath(), 'slimserver.pref')) { push @dirs, catdir($class->writablePath(''), 'slimserver.pref'); } - + elsif (-r catdir($Bin, 'slimserver.pref')) { push @dirs, catdir($Bin, 'slimserver.pref'); @@ -196,14 +196,14 @@ sub dirsFor { my $path; - # Windows Home Server offers a Music share which is more likely to be used + # Windows Home Server offers a Music share which is more likely to be used # than the administrator's My Music folder # XXX - should we continue to support WHS? if ($class->{osDetails}->{isWHS} && $dir =~ /^(?:music|playlists)$/) { my $objWMI = Win32::OLE->GetObject('winmgmts://./root/cimv2'); - + if ( $objWMI && (my $shares = $objWMI->InstancesOf('Win32_Share')) ) { - + my $path2; foreach my $objShare (in $shares) { @@ -220,20 +220,20 @@ sub dirsFor { $path2 = $objShare->Path; } } - + undef $shares; - + # we didn't find x:\shares\music, but some other share with music in the path if ($path2 && !$path) { $path = $path2; } } - + undef $objWMI; } my $fallback; - + if ($dir =~ /^(?:music|playlists)$/) { $path = Win32::GetFolderPath(Win32::CSIDL_MYMUSIC) unless $path; $fallback = 'My Music'; @@ -246,18 +246,18 @@ sub dirsFor { $path = Win32::GetFolderPath(Win32::CSIDL_MYPICTURES) unless $path; $fallback = 'My Pictures'; } - + # fall back if no path or invalid path is returned if (!$path || $path eq Win32::GetFolderPath(0)) { - + my $swKey = $Win32::TieRegistry::Registry->Open( - 'CUser/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders/', - { - Access => Win32::TieRegistry::KEY_READ(), - Delimiter =>'/' + 'CUser/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders/', + { + Access => Win32::TieRegistry::KEY_READ(), + Delimiter =>'/' } ); - + if (defined $swKey) { if (!($path = $swKey->{$fallback})) { if ($path = $swKey->{'Personal'}) { @@ -301,30 +301,30 @@ sub getFileName { my $locale = Slim::Utils::Unicode->currentLocale(); my $fsObj; - + if ($locale ne 'cp1252') { $fsObj = Win32::OLE->new('Scripting.FileSystemObject') or Slim::Utils::Log::logger('database.info')->error("$@ - cannot load Scripting.FileSystemObject?!?"); } - - # display full name if we got a Windows 8.3 file name - if ($path =~ /~/) { + + # display full name if we got a Windows 8.3 file path with a tilde in it + if ($path =~ /~\d+(\.|$)/ && $path =~ /\\|\//) { if (my $n = Win32::GetLongPathName($path)) { $n = File::Basename::basename($n); main::INFOLOG && Slim::Utils::Log::logger('database.info')->info("Expand short name returned by readdir() to full name: $path -> $n"); - + $path = $n; } } - elsif ( $locale ne 'cp1252' && $fsObj && -d $path && (my $folderObj = $fsObj->GetFolder($path)) ) { + elsif ( $fsObj && -d $path && (my $folderObj = $fsObj->GetFolder($path)) ) { main::INFOLOG && Slim::Utils::Log::logger('database.info')->info("Running Windows with non-Western codepage, trying to convert folder name: $path -> " . $folderObj->{Name}); $path = $folderObj->{Name}; } - elsif ( $locale ne 'cp1252' && $fsObj && -f $path && (my $fileObj = $fsObj->GetFile($path)) ) { + elsif ( $fsObj && -f $path && (my $fileObj = $fsObj->GetFile($path)) ) { main::INFOLOG && Slim::Utils::Log::logger('database.info')->info("Running Windows with non-Western codepage, trying to convert file name: $path -> " . $fileObj->{Name}); $path = $fileObj->{Name}; @@ -332,11 +332,11 @@ sub getFileName { else { # bug 16683 - experimental fix - # Decode pathnames that do not have '~' as they may have locale-encoded chracaters in them + # Decode pathnames that do not have '~' as they may have locale-encoded characters in them $path = Slim::Utils::Unicode::utf8decode_locale($path); } - return $path; + return $path; } sub scanner { @@ -362,7 +362,7 @@ sub localeDetails { my $lc_ctype = "cp$linfo"; my $locale = Win32::Locale::get_locale($langid); my $lc_time = POSIX::setlocale(LC_TIME, $locale); - + return ($lc_ctype, $lc_time); } @@ -371,7 +371,7 @@ sub getSystemLanguage { require Win32::Locale; - $class->_parseLanguage(Win32::Locale::get_language()); + $class->_parseLanguage(Win32::Locale::get_language()); } sub dontSetUserAndGroup { 1 } @@ -383,9 +383,9 @@ sub getProxy { # on Windows read Internet Explorer's proxy setting my $ieSettings = $Win32::TieRegistry::Registry->Open( 'CUser/Software/Microsoft/Windows/CurrentVersion/Internet Settings', - { - Access => Win32::TieRegistry::KEY_READ(), - Delimiter =>'/' + { + Access => Win32::TieRegistry::KEY_READ(), + Delimiter =>'/' } ); @@ -396,13 +396,24 @@ sub getProxy { return $proxy || $class->SUPER::getProxy(); } +sub getDefaultGateway { + my $route = `route print -4`; + while ( $route =~ /^\s*0\.0\.0\.0\s+\d+\.\d+\.\d+\.\d+\s+(\d+\.\d+\.\d+\.\d+)/mg ) { + if ( Slim::Utils::Network::ip_is_private($1) ) { + return $1; + } + } + + return; +} + sub ignoredItems { return ( # Items we should ignore on a Windows volume 'System Volume Information' => '/', 'RECYCLER' => '/', 'Recycled' => '/', - '$Recycle.Bin' => '/', + '$Recycle.Bin' => '/', ); } @@ -417,20 +428,20 @@ sub getDrives { if (!defined $driveList->{ttl} || !$driveList->{drives} || $driveList->{ttl} < time) { require Win32API::File;; - + my @drives = grep { s/\\//; - + my $driveType = Win32API::File::GetDriveType($_); Slim::Utils::Log::logger('os.paths')->debug("Drive of type '$driveType' found: $_"); - + # what USB drive is considered REMOVABLE, what's FIXED? # have an external HDD -> FIXED, USB stick -> REMOVABLE # would love to filter out REMOVABLEs, but I'm not sure it's save #($driveType != DRIVE_UNKNOWN && $driveType != DRIVE_REMOVABLE); ($driveType != Win32API::File->DRIVE_UNKNOWN && /[^AB]:/i); } Win32API::File::getLogicalDrives(); - + $driveList = { ttl => time() + 60, drives => \@drives @@ -449,7 +460,7 @@ Verifies whether a drive can be accessed or not sub isDriveReady { my ($class, $drive) = @_; - # shortcut - we've already tested this drive + # shortcut - we've already tested this drive if (!defined $driveState->{$drive} || $driveState->{$drive}->{ttl} < time) { $driveState->{$drive} = { @@ -465,7 +476,7 @@ sub isDriveReady { Slim::Utils::Log::logger('os.paths')->debug("Checking drive state for $drive"); Slim::Utils::Log::logger('os.paths')->debug(' --> ' . ($driveState->{$drive}->{state} ? 'ok' : 'nok')); } - + return $driveState->{$drive}->{state}; } @@ -480,10 +491,10 @@ sub installPath { # Try and find it in the registry. # This is a system-wide registry key. my $swKey = $Win32::TieRegistry::Registry->Open( - 'LMachine/Software/Logitech/Squeezebox/', - { - Access => Win32::TieRegistry::KEY_READ(), - Delimiter =>'/' + 'LMachine/Software/Logitech/Squeezebox/', + { + Access => Win32::TieRegistry::KEY_READ(), + Delimiter =>'/' } ); @@ -505,7 +516,7 @@ sub installPath { } return $installDir || getcwd(); - + return ''; } @@ -525,13 +536,13 @@ sub writablePath { # the installer is writing the data folder to the registry - give this the first try my $swKey = $Win32::TieRegistry::Registry->Open( - 'LMachine/Software/Logitech/Squeezebox/', - { - Access => Win32::TieRegistry::KEY_READ(), - Delimiter =>'/' + 'LMachine/Software/Logitech/Squeezebox/', + { + Access => Win32::TieRegistry::KEY_READ(), + Delimiter =>'/' } ); - + if (defined $swKey && $swKey->{'DataPath'}) { $writablePath = $swKey->{'DataPath'}; } @@ -540,7 +551,7 @@ sub writablePath { # second attempt: use the Windows API (recommended by MS) # use the "Common Application Data" folder to store Logitech Media Server configuration etc. $writablePath = Win32::GetFolderPath(Win32::CSIDL_COMMON_APPDATA); - + # fall back if no path or invalid path is returned if (!$writablePath || $writablePath eq Win32::GetFolderPath(0)) { @@ -548,17 +559,17 @@ sub writablePath { # NOTE: this key has proved to be wrong on some Vista systems # only here for backwards compatibility $swKey = $Win32::TieRegistry::Registry->Open( - 'LMachine/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders/', - { - Access => Win32::TieRegistry::KEY_READ(), - Delimiter =>'/' + 'LMachine/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders/', + { + Access => Win32::TieRegistry::KEY_READ(), + Delimiter =>'/' } ); - + if (defined $swKey && $swKey->{'Common AppData'}) { $writablePath = $swKey->{'Common AppData'}; } - + elsif ($ENV{'ProgramData'}) { $writablePath = $ENV{'ProgramData'}; } @@ -568,17 +579,17 @@ sub writablePath { $writablePath = $Bin; } } - + $writablePath = catdir($writablePath, 'Squeezebox') unless $writablePath eq $Bin; - + # store the key in the registry for future reference $swKey = $Win32::TieRegistry::Registry->Open( - 'LMachine/Software/Logitech/Squeezebox/', - { - Delimiter =>'/' + 'LMachine/Software/Logitech/Squeezebox/', + { + Delimiter =>'/' } ); - + if (defined $swKey && !$swKey->{'DataPath'}) { $swKey->{'DataPath'} = $writablePath; } @@ -631,14 +642,14 @@ sub pathFromShortcut { } else { Slim::Utils::Log::logger('os.files')->error("Bad path in $fullpath - path was: [$path]"); - + return; } } else { Slim::Utils::Log::logger('os.files')->error("Shortcut $fullpath is invalid"); - + return; } @@ -664,10 +675,10 @@ sub fileURLFromShortcut { sub getShortcut { my ($class, $path) = @_; - + my $name = Slim::Music::Info::fileName($path); $name =~ s/\.lnk$//i; - + return ( $name, $class->fileURLFromShortcut($path) ); } @@ -677,128 +688,15 @@ Set the priority for the server. $priority should be -20 to 20 =cut -sub setPriority { - my ($class, $priority) = @_; - - return unless defined $priority && $priority =~ /^-?\d+$/; - - Slim::bootstrap::tryModuleLoad('Scalar::Util', 'Win32::API', 'Win32::Process', 'nowarn'); - - # For win32, translate the priority to a priority class and use that - my ($priorityClass, $priorityClassName) = _priorityClassFromPriority($priority); - - my $getCurrentProcess = Win32::API->new('kernel32', 'GetCurrentProcess', ['V'], 'N'); - my $setPriorityClass = Win32::API->new('kernel32', 'SetPriorityClass', ['N', 'N'], 'N'); - - if (Scalar::Util::blessed($setPriorityClass) && Scalar::Util::blessed($getCurrentProcess)) { - - my $processHandle = eval { $getCurrentProcess->Call(0) }; - - if (!$processHandle || $@) { - - Slim::Utils::Log->logError("Can't get process handle ($^E) [$@]"); - return; - }; - - Slim::Utils::Log::logger('server')->info("Logitech Media Server changing process priority to $priorityClassName"); - - eval { $setPriorityClass->Call($processHandle, $priorityClass) }; - - if ($@) { - Slim::Utils::Log->logError("Couldn't set priority to $priorityClassName ($^E) [$@]"); - } - } -} +sub setPriority {} =head2 getPriority( ) -Get the current priority of the server. - -=cut - -sub getPriority { - return _priorityFromPriorityClass( getPriorityClass() ); -} - -=head1 getPriorityClass() - -Get the current Win32 priority class of the server. +Get the current priority of the server. Disabled on Windows. =cut -sub getPriorityClass { - Slim::bootstrap::tryModuleLoad('Scalar::Util', 'Win32::API', 'Win32::Process', 'nowarn'); - - my $getCurrentProcess = Win32::API->new('kernel32', 'GetCurrentProcess', ['V'], 'N'); - my $getPriorityClass = Win32::API->new('kernel32', 'GetPriorityClass', ['N'], 'N'); - - if (Scalar::Util::blessed($getPriorityClass) && Scalar::Util::blessed($getCurrentProcess)) { - - my $processHandle = eval { $getCurrentProcess->Call(0) }; - - if (!$processHandle || $@) { - - Slim::Utils::Log->logError("Can't get process handle ($^E) [$@]"); - return; - }; - - my $priorityClass = eval { $getPriorityClass->Call($processHandle) }; - - if ($@) { - Slim::Utils::Log->logError("Can't get priority class ($^E) [$@]"); - } - - return $priorityClass; - } - - return; -} - -# Translation between win32 and *nix priorities -# is as follows: -# -20 - -16 HIGH -# -15 - -6 ABOVE NORMAL -# -5 - 4 NORMAL -# 5 - 14 BELOW NORMAL -# 15 - 20 LOW - -sub _priorityClassFromPriority { - my $priority = shift; - - # ABOVE_NORMAL_PRIORITY_CLASS and BELOW_NORMAL_PRIORITY_CLASS aren't - # provided by Win32::Process so their values have been hardcoded. - - if ($priority <= -16 ) { - return (Win32::Process::HIGH_PRIORITY_CLASS(), "HIGH"); - } elsif ($priority <= -6) { - return (0x00008000, "ABOVE_NORMAL"); - } elsif ($priority <= 4) { - return (Win32::Process::NORMAL_PRIORITY_CLASS(), "NORMAL"); - } elsif ($priority <= 14) { - return (0x00004000, "BELOW_NORMAL"); - } else { - return (Win32::Process::IDLE_PRIORITY_CLASS(), "LOW"); - } -} - -sub _priorityFromPriorityClass { - my $priorityClass = shift; - - if ($priorityClass == 0x00000100) { # REALTIME - return -20; - } elsif ($priorityClass == Win32::Process::HIGH_PRIORITY_CLASS()) { - return -16; - } elsif ($priorityClass == 0x00008000) { - return -6; - } elsif ($priorityClass == 0x00004000) { - return 5; - } elsif ($priorityClass == Win32::Process::IDLE_PRIORITY_CLASS()) { - return 15; - } else { - return 0; - } -} - +sub getPriority {} =head2 cleanupTempDirs( ) @@ -810,9 +708,9 @@ if process is crashing. Use this method to clean them up. sub cleanupTempDirs { my $dir = $ENV{TEMP}; - + return unless $dir && -d $dir; - + opendir(DIR, $dir) || return; my @folders = readdir(DIR); @@ -824,16 +722,16 @@ sub cleanupTempDirs { $pdkFolders{$1} = $entry } } - + return unless scalar(keys %pdkFolders); require File::Path; require Win32::Process::List; my $p = Win32::Process::List->new(); - my %processes = $p->GetProcesses(); + my %processes = $p->GetProcesses(); foreach my $pid (keys %pdkFolders) { - + # don't remove files if process is still running... next if $processes{$pid}; @@ -849,16 +747,16 @@ sub getUpdateParams { my ($class, $url) = @_; return if main::SCANNER; - + if (!$PerlSvc::VERSION) { Slim::Utils::Log::logger('server.update')->info("Running Logitech Media Server from the source - don't download the update."); return; } - + require Win32::NetResource; - + my $downloaddir; - + if ($class->{osDetails}->{isWHS}) { my $share; @@ -868,7 +766,7 @@ sub getUpdateParams { if (!$share || !$share->{path}) { Win32::NetResource::NetShareGetInfo('logiciel', $share); } - + if ($share && $share->{path}) { $downloaddir = $share->{path}; @@ -877,7 +775,7 @@ sub getUpdateParams { } } } - + return { path => $downloaddir, }; @@ -886,8 +784,8 @@ sub getUpdateParams { sub canAutoUpdate { 1 } # return file extension filter for installer -sub installerExtension { '(?:exe|msi)' }; -sub installerOS { +sub installerExtension { '(?:exe|msi)' }; +sub installerOS { my $class = shift; return $class->{osDetails}->{isWHS} ? 'whs' : 'win'; } @@ -896,13 +794,13 @@ sub restartServer { my $class = shift; my $log = Slim::Utils::Log::logger('server.update'); - + if (!$class->canRestartServer()) { $log->warn("Logitech Media Server can't be restarted automatically on Windows if run from the perl source."); return; } - + if ($PerlSvc::VERSION && PerlSvc::RunningAsService()) { my $svcHelper = Win32::GetShortPathName( catdir( $class->installPath, 'server', 'squeezesvc.exe' ) ); @@ -924,16 +822,16 @@ sub restartServer { return 1; } } - + elsif ($PerlSvc::VERSION) { - + my $restartFlag = catdir( Slim::Utils::Prefs::preferences('server')->get('cachedir') || $class->dirsFor('cache'), 'restart.txt' ); if (open(RESTART, ">$restartFlag")) { close RESTART; main::stopServer(); return 1; } - + else { $log->error("Can't write restart flag ($restartFlag) - don't shut down"); } diff --git a/Slim/Utils/OSDetect.pm b/Slim/Utils/OSDetect.pm index 91fe1500c65..b6bdfb8755e 100644 --- a/Slim/Utils/OSDetect.pm +++ b/Slim/Utils/OSDetect.pm @@ -1,10 +1,9 @@ package Slim::Utils::OSDetect; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. =head1 NAME @@ -46,7 +45,7 @@ sub OS { sub init { my $newBin = shift; - + if ($os) { return; } @@ -55,7 +54,7 @@ sub init { if (defined $newBin && -d $newBin) { $Bin = $newBin; } - + # Let's see whether there's a custom OS file (to be used by 3rd party NAS vendors etc.) eval { require Slim::Utils::OS::Custom; @@ -70,64 +69,59 @@ sub init { if (!$os) { if ($^O =~/darwin/i) { - + require Slim::Utils::OS::OSX; $os = Slim::Utils::OS::OSX->new(); - + } elsif ($^O =~ /^m?s?win/i) { - + require Slim::Utils::OS::Win32; $os = Slim::Utils::OS::Win32->new(); - + } elsif ($^O =~ /linux/i) { - + require Slim::Utils::OS::Linux; $os = Slim::Utils::OS::Linux->getFlavor(); - + if ($os =~ /RAIDiator/i) { - + require Slim::Utils::OS::ReadyNAS; $os = Slim::Utils::OS::ReadyNAS->new(); - + # we only differentiate Debian/Suse/Red Hat if they've been installed from a package } elsif ($os =~ /debian/i && $0 =~ m{^/usr/sbin/squeezeboxserver}) { - + require Slim::Utils::OS::Debian; $os = Slim::Utils::OS::Debian->new(); - + } elsif ($os =~ /red hat/i && $0 =~ m{^/usr/libexec/squeezeboxserver}) { - + require Slim::Utils::OS::RedHat; $os = Slim::Utils::OS::RedHat->new(); - + } elsif ($os =~ /suse/i && $0 =~ m{^/usr/libexec/squeezeboxserver}) { - + require Slim::Utils::OS::Suse; $os = Slim::Utils::OS::Suse->new(); - } elsif ($os =~ /Synology/i) { + } elsif ($os =~ /Synology/i) { - require Slim::Utils::OS::Synology; - $os = Slim::Utils::OS::Synology->new(); + require Slim::Utils::OS::Synology; + $os = Slim::Utils::OS::Synology->new(); - } elsif ($os =~ /squeezeos/i) { - - require Slim::Utils::OS::SqueezeOS; - $os = Slim::Utils::OS::SqueezeOS->new(); - } else { - + $os = Slim::Utils::OS::Linux->new(); } - + } else { - + require Slim::Utils::OS::Unix; $os = Slim::Utils::OS::Unix->new(); - + } } - + $os->initDetails(); $isWindows = $os->name eq 'win'; $isMac = $os->name eq 'mac'; @@ -176,10 +170,6 @@ sub isRHorSUSE { return $os->get('isRedHat', 'isSuse'); } -sub isSqueezeOS { - return $os->get('isSqueezeOS'); -} - sub isWindows { return $isWindows; } diff --git a/Slim/Utils/PerfMon.pm b/Slim/Utils/PerfMon.pm index 79f4a18bb20..9960a9eb58d 100644 --- a/Slim/Utils/PerfMon.pm +++ b/Slim/Utils/PerfMon.pm @@ -1,6 +1,5 @@ package Slim::Utils::PerfMon; -# $Id$ # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Utils/PerlRunTime.pm b/Slim/Utils/PerlRunTime.pm index 8dfe2944782..ba46f3251bb 100644 --- a/Slim/Utils/PerlRunTime.pm +++ b/Slim/Utils/PerlRunTime.pm @@ -1,8 +1,7 @@ package Slim::Utils::PerlRunTime; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/PluginDownloader.pm b/Slim/Utils/PluginDownloader.pm index 8e198a366f8..2c1c2d581a8 100644 --- a/Slim/Utils/PluginDownloader.pm +++ b/Slim/Utils/PluginDownloader.pm @@ -1,11 +1,15 @@ package Slim::Utils::PluginDownloader; +# Logitech Media Server Copyright 2001-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + # Plugins are downloaded to /DownloadedPlugins and then extracted to /InstalledPlugins/Plugins/ # # Plugins zip files should not include any additional path information - i.e. they include the install.xml file at the top level # The plugin 'name' must match the package naming of the plugin, i.e. name 'MyPlugin' equates to package 'Plugins::MyPlugin::Plugin' -# $Id$ use strict; @@ -87,7 +91,7 @@ sub extract { if (-d $targetDir) { main::INFOLOG && $log->info("removing existing $targetDir"); - + rmtree $targetDir; } @@ -113,16 +117,16 @@ sub extract { # ignore additional directory information in zip for my $search ("Plugins/$plugin/", "$plugin/") { - + if ( $zip->membersMatching("^$search") ) { - + $source = $search; last; } } - + if ( ($zipstatus = $zip->extractTree($source, "$targetDir/")) == Archive::Zip::AZ_OK() ) { - + main::INFOLOG && $log->info("extracted $plugin to $targetDir"); } else { @@ -152,15 +156,28 @@ sub install { my $file = catdir($downloadTo, "$name.zip"); - unlink $file; + if (-r $file) { + my $digest = _getDigest($file); + + if ($args->{sha} ne $digest) { + main::INFOLOG && $log->is_info && $log->info(sprintf("found existing download, but digest does not match %s - %s will be re-downloaded: expected %s, got %s", $file, $name, $args->{sha}, $digest)); + unlink $file; + } + } + + if (-r $file) { + main::DEBUGLOG && $log->is_debug && $log->debug("found existing download, digest matches - don't re-download: $name"); + _installDownload($file, $args); + } + else { + my $http = Slim::Networking::SimpleAsyncHTTP->new( \&_downloadDone, \&_downloadError, { saveAs => $file, args => $args } ); - my $http = Slim::Networking::SimpleAsyncHTTP->new( \&_downloadDone, \&_downloadError, { saveAs => $file, args => $args } ); - - main::INFOLOG && $log->info("install - downloading $name from $url"); + main::INFOLOG && $log->info("install - downloading $name from $url"); - $downloading++; + $downloading++; - $http->get($url); + $http->get($url); + } } sub _downloadDone { @@ -170,31 +187,41 @@ sub _downloadDone { my $args = $http->params('args'); my $name = $args->{'name'}; - my $digest= $args->{'sha'}; - my $url = $http->url; main::INFOLOG && $log->info("downloaded $name to $file"); $downloading--; + _installDownload($file, $args); +} + +sub _installDownload { + my ($file, $args) = @_; + + my $name = $args->{'name'}; + my $digest= $args->{'sha'}; + if (-r $file) { - my $sha1 = Digest::SHA1->new; - - open my $fh, '<', $file; + my $gotDigest = _getDigest($file); - binmode $fh; - - $sha1->addfile($fh); - - close $fh; - - if ($digest ne $sha1->hexdigest) { - - $log->warn("digest does not match $file - $name will not be installed"); + if ($digest ne $gotDigest) { + + $log->error("digest does not match $file - $name will not be installed: expected $digest, got " . $gotDigest); + + if (main::DEBUGLOG && $log->is_debug && (my $cacheDir = preferences('server')->get('cachedir'))) { + require File::Basename; + require File::Copy; + + my ($filename, undef, $ext) = File::Basename::fileparse($file, '.zip'); + my $backup = catfile($cacheDir, $filename . '-' . $gotDigest . $ext); + $log->debug("Moving downloaded file for you to investigate:\n from: $file\n to: $backup"); + File::Copy::move($file, $backup); + } + else { + unlink $file; + } - unlink $file; - } else { main::INFOLOG && $log->info("digest matches - scheduling $name for install on restart"); @@ -204,6 +231,22 @@ sub _downloadDone { } } +sub _getDigest { + my ($file) = @_; + + my $sha1 = Digest::SHA1->new; + + open my $fh, '<', $file or $log->error("Failed to open $file"); + + binmode $fh; + + $sha1->addfile($fh); + + close $fh; + + return $sha1->hexdigest; +} + sub _downloadError { my $http = shift; my $error = shift; @@ -281,7 +324,7 @@ sub _handleResponse { Slim::Utils::PluginManager->message( sprintf( "%s (%s)", Slim::Utils::Strings::string('PLUGINS_UPDATES_AVAILABLE'), $updates ) ); - + # $updates is only set if we don't want to auto-update return; } @@ -293,13 +336,13 @@ sub _handleResponse { main::INFOLOG && $log->info("ignoring response - $plugin already pending action: " . $prefs->get($plugin)); next; } - + my $entry = $actions->{$plugin}; if ($entry->{'action'} eq 'install' && $entry->{'url'} && $entry->{'sha'}) { $class->install({ name => $plugin, url => $entry->{'url'}, sha => $entry->{'sha'} }); - + } elsif ($entry->{'action'} eq 'uninstall') { $class->uninstall($plugin); diff --git a/Slim/Utils/PluginManager.pm b/Slim/Utils/PluginManager.pm index 2e9ee168f77..b882db17cba 100644 --- a/Slim/Utils/PluginManager.pm +++ b/Slim/Utils/PluginManager.pm @@ -1,11 +1,10 @@ package Slim::Utils::PluginManager; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. # -# $Id$ use strict; @@ -210,7 +209,7 @@ sub load { # in failsafe mode skip all plugins which aren't required next if ($main::failsafe && !$plugins->{$name}->{'enforce'}); - if ( main::NOMYSB && $plugins->{$name}->{needsMySB} ) { + if ( main::NOMYSB && ($plugins->{$name}->{needsMySB} && $plugins->{$name}->{needsMySB} !~ /false|no/i) ) { main::INFOLOG && $log->info("Skipping plugin: $name - requires mysqueezebox.com, but support for mysqueezebox.com is disabled."); next; } @@ -336,11 +335,15 @@ sub load { my $binDir = catdir($baseDir, 'Bin'); if (-d $binDir) { + Slim::Utils::OSDetect::getOS()->initSearchPath($binDir); + # XXXX - this is legacy code, as some Slim::Utils::OS::Custom classes + # might not be updated to pass on the $binDir to initSearchPath main::DEBUGLOG && $log->debug("Adding Bin directory: [$binDir]"); - my $binArch = Slim::Utils::OSDetect::details()->{'binArch'}; - my @paths = ( catdir($binDir, $binArch), $binDir ); + my $osDetails = Slim::Utils::OSDetect::details(); + my $binArch = $osDetails->{'binArch'}; + my @paths = ( catdir($binDir, $binArch), catdir($binDir, $^O), $binDir ); if ( $binArch =~ /i386-linux/i ) { my $arch = $Config::Config{'archname'}; @@ -352,6 +355,10 @@ sub load { elsif ( $binArch && $binArch eq 'armhf-linux' ) { push @paths, catdir($binDir, 'arm-linux'); } + elsif ( $binArch =~ /darwin/i && ($osDetails->{osArch} =~ /x86_64/ || $osDetails->{osName} =~ /\b10\.[1-9][4-9]\./) ) { + unshift @paths, catdir($binDir, $^O . '-' . $osDetails->{osArch}); + unshift @paths, catdir($binDir, $binArch . '-' . $osDetails->{osArch}); + } Slim::Utils::Misc::addFindBinPaths( @paths ); } diff --git a/Slim/Utils/Prefs.pm b/Slim/Utils/Prefs.pm index 9c696607fa3..5d6af56e51e 100644 --- a/Slim/Utils/Prefs.pm +++ b/Slim/Utils/Prefs.pm @@ -1,9 +1,8 @@ package Slim::Utils::Prefs; -# $Id$ # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. =head1 NAME @@ -55,7 +54,7 @@ Preferences for plugins are expected to be stored in namespaces prefixed by 'plu =item on change callback to execute when a preference is set -=back +=back =head2 Each namespace supports: @@ -129,7 +128,7 @@ sub namespaces { sub init { my $sqlHelperClass = $os->sqlHelperClass(); my $default_dbsource = $sqlHelperClass->default_dbsource(); - + my %defaults = ( # Server Prefs not settable from web pages 'bindAddress' => '127.0.0.1', # Default MySQL bind address @@ -137,6 +136,8 @@ sub init { 'dbusername' => 'slimserver', 'dbpassword' => '', 'dbhighmem' => sub { $os->canDBHighMem() }, + # assuming that a system which has a lot of memory has larger storage, too + 'dbjournalsize' => sub { $os->canDBHighMem() ? 50 : 5 }, 'cachedir' => \&defaultCacheDir, 'librarycachedir' => \&defaultCacheDir, 'securitySecret' => \&makeSecuritySecret, @@ -219,6 +220,7 @@ sub init { return join(',', Slim::Utils::Network::hostAddr()); }, 'csrfProtectionLevel' => 0, + 'protectSettings' => 1, 'authorize' => 0, 'username' => '', 'password' => '', @@ -259,16 +261,16 @@ sub init { # Bug 5557, disable UPnP support by default 'noupnp' => 1, ); - + if (!main::NOMYSB) { # Server Settings - mysqueezebox.com $defaults{'sn_sync'} = 1; $defaults{'sn_disable_stats'} = 1; } - # we can have different defaults depending on the OS + # we can have different defaults depending on the OS $os->initPrefs(\%defaults); - + # add entry to dispatch table if it is loaded (it isn't in scanner.pl) as migration may call notify for this # this is required as Slim::Control::Request::init will not have run at this point if (exists &Slim::Control::Request::addDispatch) { @@ -282,11 +284,12 @@ sub init { # initialise any new prefs $prefs->init(\%defaults, 'Slim::Utils::Prefs::Migration'); - + # perform OS-specific post-init steps $os->postInitPrefs($prefs); # set validation functions + $prefs->setValidate( 'int', qw(dbhighmem dbjournalsize) ); $prefs->setValidate( 'num', qw(displaytexttimeout browseagelimit remotestreamtimeout screensavertimeout itemsPerPage refreshRate thumbSize httpport bufferSecs remotestreamtimeout) ); $prefs->setValidate( 'dir', qw(cachedir librarycachedir playlistdir artfolder) ); @@ -295,7 +298,7 @@ sub init { # allow users to set a port below 1024 on windows which does not require admin for this my $minP = main::ISWINDOWS ? 1 : 1024; $prefs->setValidate({ 'validator' => 'intlimit', 'low' => $minP,'high'=> 65535 }, 'httpport' ); - + $prefs->setValidate({ 'validator' => 'intlimit', 'low' => 3, 'high' => 30 }, 'bufferSecs' ); $prefs->setValidate({ 'validator' => 'intlimit', 'low' => 1, 'high' => 4096 }, 'udpChunkSize'); $prefs->setValidate({ 'validator' => 'intlimit', 'low' => 1, }, 'itemsPerPage'); @@ -329,7 +332,7 @@ sub init { # 192.168.0.* s/\*/0/g; next if Slim::Utils::Network::ip_is_ipv4($_); - + return 0; } @@ -347,19 +350,19 @@ sub init { # try to compile the regex to validate it eval { qr/$regex/ }; - + if ($@) { return; } elsif ($regex =~ /.+\.([^.]+)$/) { my $suffix = $1; return grep(/^$suffix$/i, qw(jpg gif png jpeg)); } - + return 1; } }, 'coverArt', ); - + # mediadirs must be a list of unique, valid folders $prefs->setValidate({ validator => sub { @@ -369,7 +372,7 @@ sub init { # don't accept duplicate entries my %seen; return 0 if scalar ( grep { !$seen{$_}++ } @{$new} ) != scalar @$new; - + foreach (@{ $new }) { if (Slim::Utils::Misc::isWinDrive($_)) { # do nothing - on Windows we're going to accept a drive letter without folder @@ -385,7 +388,7 @@ sub init { # set on change functions $prefs->setChange( \&Slim::Web::HTTP::adjustHTTPPort, 'httpport' ); - + # All languages are always loaded on SN $prefs->setChange( sub { Slim::Utils::Strings::setLanguage($_[1]) }, 'language' ); @@ -401,64 +404,62 @@ sub init { Slim::Control::Request::executeRequest(undef, ['wipecache', $prefs->get('dontTriggerScanOnPrefChange') ? 'queue' : undef]) }, 'ignoredarticles'); - if ( !Slim::Utils::OSDetect::isSqueezeOS() ) { + $prefs->setChange( sub { + if ( $_[1] ) { + require Slim::Utils::Update; + Slim::Utils::Update::checkVersion(); + } + }, 'checkVersion' ); + + $prefs->setChange( sub { + if ( !$_[1] ) { + require Slim::Utils::Update; + # remove the server.version file to stop update notifications + Slim::Utils::Update::setUpdateInstaller(''); + } + }, 'autoDownloadUpdate', 'checkVersion' ); + + if ( !main::SCANNER ) { $prefs->setChange( sub { - if ( $_[1] ) { - require Slim::Utils::Update; - Slim::Utils::Update::checkVersion(); + return if Slim::Music::Import->stillScanning; + + my $newValues = $_[1]; + my $oldValues = $_[3]; + + my @new = grep { + !defined $oldValues->{$_}; + } keys %$newValues; + + # trigger artwork scan if we've got a new specification only + if ( scalar @new ) { + require Slim::Music::Artwork; + + Slim::Music::Import->setIsScanning('PRECACHEARTWORK_PROGRESS'); + Slim::Music::Artwork->precacheAllArtwork(sub { + Slim::Music::Import->setIsScanning(0); + }, 1); } - }, 'checkVersion' ); + }, 'customArtSpecs'); $prefs->setChange( sub { - if ( !$_[1] ) { - require Slim::Utils::Update; - # remove the server.version file to stop update notifications - Slim::Utils::Update::setUpdateInstaller(''); - } - }, 'autoDownloadUpdate', 'checkVersion' ); - - if ( !main::SCANNER ) { - $prefs->setChange( sub { - return if Slim::Music::Import->stillScanning; - - my $newValues = $_[1]; - my $oldValues = $_[3]; - - my @new = grep { - !defined $oldValues->{$_}; - } keys %$newValues; - - # trigger artwork scan if we've got a new specification only - if ( scalar @new ) { - require Slim::Music::Artwork; - - Slim::Music::Import->setIsScanning('PRECACHEARTWORK_PROGRESS'); - Slim::Music::Artwork->precacheAllArtwork(sub { - Slim::Music::Import->setIsScanning(0); - }, 1); - } - }, 'customArtSpecs'); - - $prefs->setChange( sub { - my $new = $_[1]; - my $old = $_[3]; - Slim::Music::Import->nextScanTask if $old && !$new; - }, 'dontTriggerScanOnPrefChange' ); - } + my $new = $_[1]; + my $old = $_[3]; + Slim::Music::Import->nextScanTask if $old && !$new; + }, 'dontTriggerScanOnPrefChange' ); } if ( !main::SCANNER ) { $prefs->setChange( sub { my $newValues = $_[1]; my $oldValues = $_[3]; - + my %new = map { $_ => 1 } @$newValues; - + # get old paths which no longer exist: my @old = grep { delete $new{$_} != 1; } @$oldValues; - + # in order to get rid of stale entries trigger full rescan if path has been removed if (scalar @old) { main::INFOLOG && logger('scan.scanner')->info('removed folder from mediadirs - trigger wipecache: ' . Data::Dump::dump(@old)); @@ -477,9 +478,9 @@ sub init { $prefs->setChange( sub { my $newValues = $_[1]; my $oldValues = $_[3]; - + my %old = map { $_ => 1 } @$oldValues; - + # get new exclusion paths which did not exist previously: my @new = grep { delete $old{$_} != 1; @@ -512,7 +513,7 @@ sub init { Slim::Music::PlaylistFolderScan->init; Slim::Control::Request::executeRequest(undef, ['rescan', 'playlists']); }, 'playlistdir'); - + $prefs->setChange( sub { if ($_[1]) { Slim::Control::Request::subscribe(\&Slim::Player::Playlist::modifyPlaylistCallback, [['playlist']]); @@ -533,7 +534,7 @@ sub init { $prefs->setChange( sub { Slim::Control::Queries->wipeCaches(); - }, 'browseagelimit'); + }, 'browseagelimit', 'ignoreDirRE'); } $prefs->setChange( sub { @@ -550,7 +551,7 @@ sub init { my $client = $_[2] || return; Slim::Player::Transporter::updateEffectsLoop($client); }, 'fxloopClock'); - + $prefs->setChange( sub { my $client = $_[2] || return; Slim::Player::Transporter::updateRolloff($client); @@ -567,12 +568,12 @@ sub init { my $client = $_[2] || return; Slim::Player::Boom::setAnalogOutMode($client); }, 'analogOutMode'); - + $prefs->setChange( sub { foreach my $client ( Slim::Player::Client::clients() ) { if ($client->isa("Slim::Player::Boom")) { $client->setRTCTime(); - } + } } }, 'timeFormat'); @@ -588,7 +589,7 @@ sub init { $cookieJar->save(); main::DEBUGLOG && logger('network.squeezenetwork')->debug( 'SN session has changed, removing cookies' ); }, 'sn_session' ); - + $prefs->setChange( sub { Slim::Utils::Timers::setTimer( $_[1], @@ -596,11 +597,11 @@ sub init { sub { my $isDisabled = shift; my $http = Slim::Networking::SqueezeNetwork->new(sub {}, sub {}); - + $http->get( $http->url( '/api/v1/stats/mark_disabled/' . $isDisabled ? 1 : 0 ) ); }, ); - + }, 'sn_disable_stats'); } @@ -678,35 +679,35 @@ sub defaultMediaDirs { my $audiodir = $prefs->get('audiodir'); $prefs->remove('audiodir') if $audiodir; - + my @mediaDirs; - + # if an audiodir had been there before, configure LMS as we did in SBS: audio only if ($audiodir) { # set mediadirs to the former audiodir push @mediaDirs, $audiodir; - + # add the audiodir to the list of sources to be ignored by the other scans defaultMediaIgnoreFolders('music', $audiodir); } - + # new LMS installation: default to all media folders else { # try to find the OS specific default folders for various media types foreach my $medium ('music', 'videos', 'pictures') { my $path = Slim::Utils::OSDetect::dirsFor($medium); - + main::DEBUGLOG && $log && $log->debug("Setting default path for medium '$medium' to '$path' if available."); - + if ($path && -d $path) { push @mediaDirs, $path; - + # ignore media from other media's scan defaultMediaIgnoreFolders($medium, $path); } } } - + return \@mediaDirs; } @@ -722,10 +723,10 @@ sub defaultMediaIgnoreFolders { foreach ( @{ $ignoreDirs{$type} } ) { my $ignoreDirs = $prefs->get($_) || []; - + push @$ignoreDirs, $dir; $prefs->set($_, $ignoreDirs); - } + } } sub defaultPlaylistDir { @@ -758,7 +759,7 @@ sub defaultCacheDir { if ((!-e $CacheDir && !-w $CacheParent) || (-e $CacheDir && !-w $CacheDir)) { $CacheDir = undef; } - + return $CacheDir; } @@ -816,14 +817,14 @@ sub maxRate { if ( $rate != 0 && logger('player.source')->is_debug ) { main::DEBUGLOG && logger('player.source')->debug(sprintf("Setting maxBitRate for %s to: %d", $client->name, $rate)); } - + # if we're the master, make sure we return the lowest common denominator bitrate. my @playergroup = ($client->syncGroupActiveMembers()); - + for my $everyclient (@playergroup) { my $otherRate = maxRate($everyclient, 1); - + # find the lowest bitrate limit of the sync group. Zero refers to no limit. $rate = ($otherRate && (($rate && $otherRate < $rate) || !$rate)) ? $otherRate : $rate; } diff --git a/Slim/Utils/Prefs/Base.pm b/Slim/Utils/Prefs/Base.pm index 53b336c8255..03873a17632 100644 --- a/Slim/Utils/Prefs/Base.pm +++ b/Slim/Utils/Prefs/Base.pm @@ -1,6 +1,5 @@ package Slim::Utils::Prefs::Base; -# $Id$ # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Utils/Prefs/Client.pm b/Slim/Utils/Prefs/Client.pm index 14ec457e8f4..de04e575401 100644 --- a/Slim/Utils/Prefs/Client.pm +++ b/Slim/Utils/Prefs/Client.pm @@ -1,6 +1,5 @@ package Slim::Utils::Prefs::Client; -# $Id$ # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, @@ -93,6 +92,13 @@ sub migrate { } } +sub hasPrefs { + my ($class, $parent, $client) = @_; + + my $clientid = blessed($client) ? $client->id : $client; + return $parent->{'prefs'}->{"$clientPreferenceTag:$clientid"} ? 1 : 0; +} + sub _root { shift->{'parent'} } sub _obj { Slim::Player::Client::getClient(shift->{'clientid'}) } diff --git a/Slim/Utils/Prefs/Migration/README.md b/Slim/Utils/Prefs/Migration/README.md new file mode 100644 index 00000000000..eb187d74f2a --- /dev/null +++ b/Slim/Utils/Prefs/Migration/README.md @@ -0,0 +1,4 @@ +IMPORTANT +========= + +Whenever you add a new migration module (be it Vx.pm or ClientVx.pm), make sure you add it to the Windows build file [squeezecenter.perlsvc](https://github.com/Logitech/slimserver-platforms/blob/public/7.9/win32/squeezecenter.perlsvc). Otherwise the Windows build will not include it in the binary and fail to load. \ No newline at end of file diff --git a/Slim/Utils/Prefs/Namespace.pm b/Slim/Utils/Prefs/Namespace.pm index 7ef119df7c3..7069ab49fb7 100644 --- a/Slim/Utils/Prefs/Namespace.pm +++ b/Slim/Utils/Prefs/Namespace.pm @@ -1,6 +1,5 @@ package Slim::Utils::Prefs::Namespace; -# $Id$ # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, diff --git a/Slim/Utils/Progress.pm b/Slim/Utils/Progress.pm index 2e8fc4e835c..08c7f0e7b65 100644 --- a/Slim/Utils/Progress.pm +++ b/Slim/Utils/Progress.pm @@ -1,8 +1,7 @@ package Slim::Utils::Progress; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. diff --git a/Slim/Utils/SQLHelper.pm b/Slim/Utils/SQLHelper.pm index 87383e4d892..492128bdcfb 100644 --- a/Slim/Utils/SQLHelper.pm +++ b/Slim/Utils/SQLHelper.pm @@ -1,6 +1,5 @@ package Slim::Utils::SQLHelper; -# $Id$ =head1 NAME diff --git a/Slim/Utils/SQLiteHelper.pm b/Slim/Utils/SQLiteHelper.pm index 5e287aa3c6d..efd45930fe8 100644 --- a/Slim/Utils/SQLiteHelper.pm +++ b/Slim/Utils/SQLiteHelper.pm @@ -1,6 +1,9 @@ package Slim::Utils::SQLiteHelper; -# $Id$ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. =head1 NAME @@ -50,24 +53,24 @@ sub default_dbsource { 'dbi:SQLite:dbname=%s' } my $serverDown = 0; use constant MAX_RETRIES => 5; -# Scanning flag is set during scanning +# Scanning flag is set during scanning my $SCANNING = 0; sub init { my ( $class, $dbh ) = @_; - + # Make sure we're running the right version of DBD::SQLite if ( $DBD::SQLite::VERSION lt 1.34 ) { die "DBD::SQLite version 1.34 or higher required\n"; } - + # Reset dbsource pref if it's not for SQLite # ... or if it's using the long filename Windows doesn't like if ( $prefs->get('dbsource') !~ /^dbi:SQLite/ || $prefs->get('dbsource') !~ /library\.db/ ) { $prefs->set( dbsource => default_dbsource() ); $prefs->set( dbsource => $class->source() ); } - + if ( !main::SCANNER ) { # Event handler for notifications from scanner process Slim::Control::Request::addDispatch( @@ -84,15 +87,15 @@ sub source { # we need to migrate long 7.6.0 file names to shorter 7.6.1 filenames: Perl/Windows can't handle the long version _migrateDBFile(catfile( $prefs->get('librarycachedir'), 'squeezebox.db' ), $dbFile); - + $source = sprintf( $prefs->get('dbsource'), $dbFile ); - + return $source; } sub on_connect_do { my $class = shift; - + my $sql = [ 'PRAGMA synchronous = OFF', 'PRAGMA journal_mode = WAL', @@ -103,11 +106,11 @@ sub on_connect_do { # Highmem we will try 20M (high) or 500M (max) 'PRAGMA cache_size = ' . $class->_cacheSize, ]; - + # Default temp_store is to create disk files to save memory # Highmem we'll let it use memory push @{$sql}, 'PRAGMA temp_store = MEMORY' if $prefs->get('dbhighmem'); - + # We create this even if main::STATISTICS is not false so that the SQL always works # Track Persistent data is in another file my $persistentdb = $class->_dbFile('persist.db'); @@ -118,15 +121,15 @@ sub on_connect_do { push @{$sql}, "ATTACH '$persistentdb' AS persistentdb"; push @{$sql}, 'PRAGMA persistentdb.journal_mode = WAL'; push @{$sql}, 'PRAGMA persistentdb.cache_size = ' . $class->_cacheSize; - + return $sql; } sub setCacheSize { my $cache_size = __PACKAGE__->_cacheSize; - + return unless Slim::Schema->hasLibrary; - + my $dbh = Slim::Schema->dbh; $dbh->do("PRAGMA cache_size = $cache_size"); $dbh->do("PRAGMA persistentdb.cache_size = $cache_size"); @@ -136,23 +139,23 @@ sub _cacheSize { my $high = $prefs->get('dbhighmem'); return 2000 if !$high; - + # scanner doesn't take advantage of a huge buffer return 20000 if main::SCANNER || $high == 1; - + # maximum memory usage for large collections and lots of memory return 500_000; } sub _migrateDBFile { my ($src, $dst) = @_; - + return if -f $dst || !-r $src; - + require File::Copy; - + main::DEBUGLOG && $log->is_debug && $log->debug("trying to rename $src to $dst"); - + if ( !File::Copy::move( $src, $dst ) ) { $log->error("Unable to rename $src to $dst: $!. Please remove it manually."); } @@ -161,29 +164,29 @@ sub _migrateDBFile { my $hasICU; my $currentICU = ''; my $loadedICU = {}; -sub collate { +sub collate { # Use ICU if built into DBD::SQLite if ( !defined $hasICU ) { $hasICU = (DBD::SQLite->can('compile_options') && grep /ENABLE_ICU/, DBD::SQLite::compile_options()); } - + if ($hasICU) { my $lang = $prefs->get('language'); my $collation = Slim::Utils::Strings::getLocales()->{$lang}; - if ( $currentICU ne $collation ) { + if ( $collation && $currentICU ne $collation ) { if ( !$loadedICU->{$collation} ) { if ( !Slim::Schema->hasLibrary() ) { # XXX for i.e. ContributorTracks many_to_many return "COLLATE $collation "; } - + # Point to our custom small ICU collation data file $ENV{ICU_DATA} = Slim::Utils::OSDetect::dirsFor('strings'); my $dbh = Slim::Schema->dbh; - + my $qcoll = $dbh->quote($collation); my $qpath = $dbh->quote($ENV{ICU_DATA}); @@ -198,18 +201,18 @@ sub collate { $hasICU = 0; return 'COLLATE perllocale '; } - + main::DEBUGLOG && $log->is_debug && $log->debug("Loaded ICU collation for $collation"); - + $loadedICU->{$collation} = 1; } - + $currentICU = $collation; } - - return "COLLATE $currentICU "; + + return "COLLATE $currentICU " if $currentICU; } - + # Fallback to built-in perllocale collation to sort using Unicode Collation Algorithm # on systems with a properly installed locale. return 'COLLATE perllocale '; @@ -256,7 +259,7 @@ Returns the version of MySQL that the $dbh is connected to. sub sqlVersion { my $class = shift; my $dbh = shift || return 0; - + return 'SQLite'; } @@ -269,7 +272,7 @@ Returns the long version string, i.e. 5.0.22-standard sub sqlVersionLong { my $class = shift; my $dbh = shift || return 0; - + return 'DBD::SQLite ' . $DBD::SQLite::VERSION . ' (sqlite ' . $dbh->{sqlite_version} . ')'; } @@ -311,7 +314,7 @@ Called after a scan is finished. Notifies main server to copy back the scanner f sub afterScan { my $class = shift; - + $class->updateProgress('end'); } @@ -323,10 +326,10 @@ Called during the Slim::Schema->optimizeDB call to run some DB specific cleanup sub optimizeDB { my $class = shift; - + # only run VACUUM in the scanner, or if no player is active return if !main::SCANNER && grep { $_->power() } Slim::Player::Client::clients(); - + $class->vacuum('library.db'); $class->vacuum('persist.db'); } @@ -339,7 +342,7 @@ Called as the scanner process exits. Used by main process to detect scanner cras sub exitScan { my $class = shift; - + $class->updateProgress('exit'); } @@ -352,31 +355,31 @@ my %postConnectHandlers; sub postConnect { my ( $class, $dbh ) = @_; - + $dbh->func( 'MD5', 1, sub { md5_hex( $_[0] ) }, 'create_function' ); - + # http://search.cpan.org/~adamk/DBD-SQLite-1.33/lib/DBD/SQLite.pm#Transaction_and_Database_Locking $dbh->{sqlite_use_immediate_transaction} = 1; - + # Reset collation load state $currentICU = ''; $loadedICU = {}; - + # Check if the DB has been optimized (stats analysis) if ( !main::SCANNER ) { # Check for the presence of the sqlite_stat1 table my ($count) = eval { $dbh->selectrow_array( "SELECT COUNT(*) FROM sqlite_stat1 WHERE tbl = 'tracks' OR tbl = 'images' OR tbl = 'videos'", undef, () ) }; - + if (!$count) { my ($table) = eval { $dbh->selectrow_array('SELECT name FROM sqlite_master WHERE type="table" AND name="tracks"') }; - + if ($table) { - $log->error('Optimizing DB because of missing or empty sqlite_stat1 table'); + $log->error('Optimizing DB because of missing or empty sqlite_stat1 table'); Slim::Schema->optimizeDB(); } } } - + foreach (keys %postConnectHandlers) { $_->postDBConnect($dbh); } @@ -390,11 +393,11 @@ Allow plugins and others to register handlers which should be called from postCo sub addPostConnectHandler { my ( $class, $handler ) = @_; - + if ($handler && $handler->can('postDBConnect')) { $postConnectHandlers{$handler}++ } - + # if we register for the first time, re-initialize the dbh object if ( $postConnectHandlers{$handler} == 1 ) { Slim::Schema->disconnect; @@ -404,59 +407,59 @@ sub addPostConnectHandler { sub updateProgress { my $class = shift; - + return if $serverDown > MAX_RETRIES; - + require LWP::UserAgent; require HTTP::Request; - + my $log = logger('scan.scanner'); - + # Scanner does not have an event loop, so use sync HTTP here. # Don't use Slim::Utils::Network, as it comes with too much overhead. my $host = ( $prefs->get('httpaddr') || '127.0.0.1' ) . ':' . $prefs->get('httpport'); - + my $ua = LWP::UserAgent->new( timeout => 5, ); - + my $req = HTTP::Request->new( POST => "http://${host}/jsonrpc.js" ); - + $req->header( 'X-Scanner' => 1 ); - + # Handle security if necessary if ( my $username = $prefs->get('username') ) { my $password = $prefs->get('password'); $req->authorization_basic($username, $password); } - + $req->content( to_json( { id => 1, method => 'slim.request', params => [ '', [ 'scanner', 'notify', @_ ] ], } ) ); - + main::INFOLOG && $log->is_info && $log->info( 'Notify to server: ' . Data::Dump::dump(\@_) ); - + my $res = $ua->request($req); if ( $res->is_success ) { if ( $res->content =~ /abort/ ) { logWarning('Server aborted scan, shutting down'); Slim::Utils::Progress->clear; - + # let the user know we aborted the scan - my $progress = Slim::Utils::Progress->new( { + my $progress = Slim::Utils::Progress->new( { type => 'importer', name => 'failure', total => 1, - every => 1, + every => 1, } ); $progress->update('SCAN_ABORTED'); - + Slim::Music::Import->setIsScanning(0); - + exit; } else { @@ -466,7 +469,7 @@ sub updateProgress { } else { main::INFOLOG && $log->is_info && $log->info( 'Notify to server failed: ' . $res->status_line ); - + if ( $res->content =~ /timeout|refused/ ) { # Server is down, avoid further requests $serverDown++; @@ -490,16 +493,16 @@ Run a given PRAGMA statement. sub pragma { my ( $class, $pragma ) = @_; - + my $dbh = Slim::Schema->dbh; $dbh->do("PRAGMA $pragma"); - + if ( $pragma =~ /locking_mode/ ) { # if changing the locking_mode we need to run a statement on each database to change the lock $dbh->do('SELECT 1 FROM metainformation LIMIT 1'); $dbh->do('SELECT 1 FROM tracks_persistent LIMIT 1'); } - + # Pass the pragma to the ArtworkCache database Slim::Utils::ArtworkCache->new->pragma($pragma); } @@ -516,11 +519,12 @@ sub vacuum { my ( $class, $db, $optional ) = @_; my $dbFile = catfile( $prefs->get('librarycachedir'), ($db || 'library.db') ); - + return unless -f $dbFile; + my $dbSize = -s _; main::DEBUGLOG && $log->is_debug && $log->debug("Start VACUUM $db"); - + my $source = sprintf( $class->default_dbsource(), $dbFile ); # this can't be run from the schema_cleanup.sql, as VACUUM doesn't work inside a transaction @@ -538,7 +542,11 @@ sub vacuum { } if ( !$optional ) { - $dbh->do('PRAGMA temp_store = MEMORY') if $prefs->get('dbhighmem'); + # use memory as temporary storage for the vaccum if dbhighmem is enabled and db file is smaller than 1GB + if ( Slim::Utils::OSDetect::getOS->canVacuumInMemory($dbSize) ) { + main::INFOLOG && $log->is_info && $log->info("Using memory (RAM) to store temporary table in VACUUM command: $db"); + $dbh->do('PRAGMA temp_store = MEMORY'); + } $dbh->do('VACUUM'); } $dbh->disconnect; @@ -548,28 +556,28 @@ sub vacuum { sub _dbFile { my ( $class, $name ) = @_; - + my ($driver, $source, $username, $password) = Slim::Schema->sourceInformation; - + my ($dbname) = $source =~ /dbname=([^;]+)/; - + return $dbname unless $name; - + my $dbbase = basename($dbname); $dbname =~ s/$dbbase/$name/; - + return $dbname; } sub _notifyFromScanner { my $request = shift; - + my $class = __PACKAGE__; - + my $msg = $request->getParam('_msg'); - + my $log = logger('scan.scanner'); - + main::INFOLOG && $log->is_info && $log->info("Notify from scanner: $msg"); # If user aborted the scan, return an abort message @@ -577,21 +585,21 @@ sub _notifyFromScanner { $request->addResult( abort => 1 ); $request->setStatusDone(); - Slim::Music::Import->setAborted(0); + Slim::Music::Import->setAborted(0); return; } - + if ( $msg eq 'start' ) { # Scanner has started $SCANNING = 1; - + if ( Slim::Utils::OSDetect::getOS->canAutoRescan && $prefs->get('autorescan') ) { require Slim::Utils::AutoRescan; Slim::Utils::AutoRescan->shutdown; } - + Slim::Music::Import->setIsScanning('SETUP_WIPEDB'); - + # XXX if scanner doesn't report in with regular progress within a set time period # assume scanner is dead. This is hard to do, as scanner may block for an indefinite # amount of time with slow network filesystems, or a large amount of files. @@ -605,27 +613,27 @@ sub _notifyFromScanner { elsif ( $msg eq 'exit' ) { # Scanner is exiting. If we get this without an 'end' message # the scanner aborted and we should throw away the scanner database - - if ( $SCANNING ) { + + if ( $SCANNING ) { $SCANNING = 0; } else { # XXX handle players with track objects that are now outdated? - + # Reconnect to the database to zero out WAL files Slim::Schema->disconnect; Slim::Schema->init; - + # Close ArtworkCache to zero out WAL file, it'll be reopened when needed Slim::Utils::ArtworkCache->new->close; } Slim::Music::Import->setIsScanning(0); - + # Clear caches, like the vaObj, etc after scanning has been finished. Slim::Control::Request::notifyFromArray( undef, [ 'rescan', 'done' ] ); } - + $request->setStatusDone(); } diff --git a/Slim/Utils/Scanner.pm b/Slim/Utils/Scanner.pm index fe7d11e1b15..898555388ee 100644 --- a/Slim/Utils/Scanner.pm +++ b/Slim/Utils/Scanner.pm @@ -1,6 +1,6 @@ package Slim::Utils::Scanner; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. diff --git a/Slim/Utils/Scanner/API.pm b/Slim/Utils/Scanner/API.pm index ce959503145..18876e33cad 100644 --- a/Slim/Utils/Scanner/API.pm +++ b/Slim/Utils/Scanner/API.pm @@ -1,6 +1,5 @@ package Slim::Utils::Scanner::API; -# $Id$ use strict; diff --git a/Slim/Utils/Scanner/LMS.pm b/Slim/Utils/Scanner/LMS.pm index fb645dbb69e..1e285967bc0 100644 --- a/Slim/Utils/Scanner/LMS.pm +++ b/Slim/Utils/Scanner/LMS.pm @@ -1,8 +1,6 @@ package Slim::Utils::Scanner::LMS; -# $Id: /sd/slim/7.6/branches/lms/server/Slim/Utils/Scanner/LMS.pm 78886 2011-07-26T15:15:45.375510Z andy $ -# -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. # diff --git a/Slim/Utils/Scanner/Local.pm b/Slim/Utils/Scanner/Local.pm index 423b92a80cc..43cf0b71623 100644 --- a/Slim/Utils/Scanner/Local.pm +++ b/Slim/Utils/Scanner/Local.pm @@ -1,8 +1,7 @@ package Slim::Utils::Scanner::Local; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. @@ -184,7 +183,7 @@ sub rescan { # if we should optimize the database or not, and also so we know if # we need to udpate lastRescanTime my $changes = 0; - my $ctFilter = $args->{types} eq 'list' ? "== 'ssp'" : "!= 'dir'"; + my $ctFilter = $args->{types} eq 'list' ? "= 'ssp'" : "!= 'dir'"; # Get list of files within this path Slim::Utils::Scanner::Local->find( $next, $args, sub { diff --git a/Slim/Utils/Scanner/Local/AIO.pm b/Slim/Utils/Scanner/Local/AIO.pm index bc6383f045b..2e26279de99 100644 --- a/Slim/Utils/Scanner/Local/AIO.pm +++ b/Slim/Utils/Scanner/Local/AIO.pm @@ -1,8 +1,7 @@ package Slim::Utils::Scanner::Local::AIO; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. @@ -25,15 +24,15 @@ use Slim::Utils::Prefs; my $log = logger('scan.scanner'); # 1 thread seems best on Touch -use constant MAX_REQS => Slim::Utils::OSDetect::isSqueezeOS() ? 1 : 8; # max threads to run +use constant MAX_REQS => 8; # max threads to run sub find { my ( $class, $path, $args, $cb ) = @_; - + main::DEBUGLOG && $log->is_debug && (my $start = AnyEvent->time); - + my $types = Slim::Music::Info::validTypeExtensions( $args->{types} || 'audio' ); - + my $progress; if ( $args->{progress} ) { $progress = Slim::Utils::Progress->new( { @@ -41,25 +40,25 @@ sub find { name => $path . '|' . ($args->{scanName} ? 'discovering_' . $args->{scanName} : 'discovering_files'), } ); } - + my $todo = 1; my @items = (); my $count = 0; my $others = []; my @dirs = (); - + my $grp = aio_group($cb); - + # Scanned files are stored in the database, use raw DBI to improve performance here my $dbh = Slim::Schema->dbh; - + my $sth = $dbh->prepare_cached( qq{ INSERT INTO scanned_files (url, timestamp, filesize) VALUES (?, ?, ?) } ); - + # Add the root directory to the database $sth->execute( Slim::Utils::Misc::fileURLFromPath($path), @@ -69,19 +68,19 @@ sub find { $grp->add( aio_readdirx( $path, IO::AIO::READDIR_STAT_ORDER, sub { my $files = shift; - + push @items, map { "$path/$_" } @{$files}; - + $todo--; - + my $childgrp = $grp->add( aio_group( sub { if ( main::DEBUGLOG && $log->is_debug ) { my $diff = sprintf "%.2f", AnyEvent->time - $start; $log->debug( "AIO scanner found $count files/dirs in $diff sec" ); } - + $progress && $progress->final($count); - + if ( $args->{dirs} ) { $grp->result( \@dirs ); } @@ -89,7 +88,7 @@ sub find { $grp->result($count, $others); } } ) ); - + $childgrp->limit(MAX_REQS); $childgrp->feed( sub { @@ -99,7 +98,7 @@ sub find { if ( $todo > 0 ) { # We still have outstanding requests, pause feeder $childgrp->limit(0); - + # If no items in queue, avoid finishing the group with a nop request my $nop; $nop = sub { @@ -113,28 +112,28 @@ sub find { return; } - + $todo++; - + $progress && $progress->update($file); - + $childgrp->add( aio_stat( $file, sub { $todo--; - + $_[0] && return; if ( -d _ ) { if ( Slim::Utils::Misc::folderFilter( $file, 0, $types ) ) { $todo++; $count++; - + # Save the dir entry in the database $sth->execute( Slim::Utils::Misc::fileURLFromPath($file), (stat _)[9], # mtime 0, # size, 0 for dirs ); - + if ( $args->{dirs} ) { push @dirs, $file; } @@ -145,7 +144,7 @@ sub find { push @items, map { "$file/$_" } @{$files}; $todo--; - + $childgrp->limit(MAX_REQS); } ) ); } @@ -156,26 +155,26 @@ sub find { if ( Slim::Utils::Misc::fileFilter( dirname($file), basename($file), $types, 0 ) ) { if ( main::ISWINDOWS && $file =~ /\.lnk$/i ) { my $orig = $file; - + my $url = Slim::Utils::Misc::fileURLFromPath($file); $url = Slim::Utils::OS::Win32->fileURLFromShortcut($url) || return; - + $file = Slim::Utils::Misc::pathFromFileURL($url); - + if ( Path::Class::dir($file)->subsumes($path) ) { $log->error("Found an infinite loop! Breaking out: $file -> $path"); return; } - + if ( !-d $file ) { return; } - + main::DEBUGLOG && $log->is_debug && $log->debug("Will follow shortcut $orig => $file"); - + push @{$others}, $file; - + return; } elsif ( @@ -186,30 +185,30 @@ sub find { (my $alias = Slim::Utils::Misc::pathFromMacAlias($file)) ) { my $orig = $file; - + $file = $alias; - + if ( Path::Class::dir($file)->subsumes($path) ) { $log->error("Found an infinite loop! Breaking out: $file -> $path"); return; } - + if ( !-d $file ) { return; } - + main::DEBUGLOG && $log->is_debug && $log->debug("Will follow alias $orig => $file"); - + push @{$others}, $file; - + return; } - + # Skip client playlists return if $args->{types} && $args->{types} =~ /list/ && $file =~ /clientplaylist.*\.m3u$/; - + $count++; - + $sth->execute( Slim::Utils::Misc::fileURLFromPath($file), (stat _)[9], # mtime diff --git a/Slim/Utils/Scanner/Local/Async.pm b/Slim/Utils/Scanner/Local/Async.pm index db472bcdad3..36fde086284 100644 --- a/Slim/Utils/Scanner/Local/Async.pm +++ b/Slim/Utils/Scanner/Local/Async.pm @@ -1,8 +1,7 @@ package Slim::Utils::Scanner::Local::Async; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. # @@ -67,6 +66,9 @@ sub find { $args->{recursive} ? Slim::Utils::Misc::folderFilter($File::Next::dir, 0, $types) : 0 }, + error_handler => sub { + $log->error('Error scanning file or folder: ', shift) + }, }, $path ); my $walk = sub { diff --git a/Slim/Utils/Scanner/Remote.pm b/Slim/Utils/Scanner/Remote.pm index cf5d23f7b4a..dc6fe9cb7bb 100644 --- a/Slim/Utils/Scanner/Remote.pm +++ b/Slim/Utils/Scanner/Remote.pm @@ -1,8 +1,7 @@ package Slim::Utils::Scanner::Remote; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2. @@ -76,15 +75,15 @@ and an error string if the scan failed. sub scanURL { my ( $class, $url, $args ) = @_; - + my $client = $args->{client}; my $cb = $args->{cb} || sub {}; my $pt = $args->{pt} || []; - + $args->{depth} ||= 0; - + main::DEBUGLOG && $log->is_debug && $log->debug( "Scanning remote stream $url" ); - + if ( !$url ) { return $cb->( undef, 'SCANNER_REMOTE_NO_URL_PROVIDED', @{$pt} ); } @@ -97,12 +96,12 @@ sub scanURL { if ( $args->{depth} >= MAX_DEPTH ) { return $cb->( undef, 'SCANNER_REMOTE_NESTED_TOO_DEEP', @{$pt} ); } - + # Get/Create a track object for this URL my $track = Slim::Schema->updateOrCreate( { url => $url, } ); - + # Make sure it has a title if ( !$track->title ) { $track = Slim::Music::Info::setTitle( $url, $args->{'title'} ? $args->{'title'} : $url ); @@ -113,40 +112,40 @@ sub scanURL { my $handler = Slim::Player::ProtocolHandlers->handlerForURL($url); if ($handler && $handler->can('scanStream') ) { main::DEBUGLOG && $log->is_debug && $log->debug( "Scanning remote stream $url using protocol hander $handler" ); - + # Allow protocol hander to scan the stream and then call the callback $handler->scanStream($url, $track, $args); return; } - + # In some cases, a remote protocol may always be audio and not need scanning # This is not used by any core code, but some plugins require it my $isAudio = Slim::Music::Info::isAudioURL($url); $url =~ s/#slim:.+$//; - - if ( $isAudio ) { - main::DEBUGLOG && $log->is_debug && $log->debug( "Remote stream $url known to be audio" ); - # Set this track's content type from protocol handler getFormatForURL method + if ( $isAudio ) { + main::DEBUGLOG && $log->is_debug && $log->debug( "Remote stream $url known to be audio" ); + + # Set this track's content type from protocol handler getFormatForURL method my $type = Slim::Music::Info::typeFromPath($url); if ( $type eq 'unk' ) { $type = 'mp3'; } - + main::DEBUGLOG && $log->is_debug && $log->debug( "Content-type of $url - $type" ); - + $track->content_type( $type ); $track->update; - # Success, done scanning + # Success, done scanning return $cb->( $track, undef, @{$pt} ); } - + # Bug 4522, if user has disabled native WMA decoding to get MMS support, don't scan MMS URLs if ( $url =~ /^mms/i ) { - + # XXX This test will not be good enough when we get WMA proxied streaming if ( main::TRANSCODING && ! Slim::Player::TranscodingHelper::isEnabled('wma-wma-*-*') ) { main::DEBUGLOG && $log->is_debug && $log->debug('Not scanning MMS URL because direct streaming disabled.'); @@ -156,31 +155,31 @@ sub scanURL { return $cb->( $track, undef, @{$pt} ); } } - + # Connect to the remote URL and figure out what it is my $request = HTTP::Request->new( GET => $url ); - + main::DEBUGLOG && $log->is_debug && $log->debug("Scanning remote URL $url"); - + # Use WMP headers for MMS protocol URLs or ASF/ASX/WMA URLs if ( $url =~ /(?:^mms|\.asf|\.asx|\.wma)/i ) { addWMAHeaders( $request ); } - + # If the URL is on SqueezeNetwork, add session headers if ( !main::NOMYSB && Slim::Networking::SqueezeNetwork->isSNURL($url) ) { my %snHeaders = Slim::Networking::SqueezeNetwork->getHeaders($client); while ( my ($k, $v) = each %snHeaders ) { $request->header( $k => $v ); } - + if ( my $snCookie = Slim::Networking::SqueezeNetwork->getCookie($client) ) { $request->header( Cookie => $snCookie ); } } - + my $timeout = preferences('server')->get('remotestreamtimeout'); - + my $send = sub { my $http = Slim::Networking::Async::HTTP->new; $http->send_request( { @@ -191,7 +190,7 @@ sub scanURL { my ( $http, $error ) = @_; logError("Can't connect to remote server to retrieve playlist for, ", $request->uri, ": $error."); - + $track->error( $error ); return $cb->( undef, $error, @{$pt} ); @@ -200,7 +199,7 @@ sub scanURL { Timeout => $timeout, } ); }; - + if ( $args->{delay} ) { Slim::Utils::Timers::setTimer( undef, Time::HiRes::time() + $args->{delay}, $send ); } @@ -219,12 +218,12 @@ http://msdn2.microsoft.com/en-us/library/cc251059.aspx sub addWMAHeaders { my $request = shift; - + my $url = $request->uri->as_string; $url =~ s/^mms/http/; - + $request->uri( $url ); - + my $h = $request->headers; $h->header( 'User-Agent' => 'NSPlayer/8.0.0.3802' ); $h->header( Pragma => [ @@ -243,24 +242,24 @@ redirects to an mms:// protocol URL we need to rewrite the link and set proper h sub handleRedirect { my ( $request, $track, $args ) = @_; - + main::DEBUGLOG && $log->is_debug && $log->debug( 'Server redirected to ' . $request->uri ); - + if ( $request->uri =~ /^mms/ ) { if ( main::DEBUGLOG && $log->is_debug ) { $log->debug("Server redirected to MMS URL: " . $request->uri . ", adding WMA headers"); } - + addWMAHeaders( $request ); } - + # Keep track of artwork or station icon across redirects my $cache = Slim::Utils::Cache->new(); if ( my $icon = $cache->get("remote_image_" . $track->url) ) { $cache->set("remote_image_" . $request->uri, $icon, '30 days'); } - + return $request; } @@ -272,16 +271,16 @@ Async callback from scanURL. The remote headers are read to determine the conte sub readRemoteHeaders { my ( $http, $track, $args ) = @_; - + my $client = $args->{client}; my $cb = $args->{cb} || sub {}; my $pt = $args->{pt} || []; # $track is the track object for the original URL we scanned # $url is the final URL, may be different due to a redirect - + my $url = $http->request->uri->as_string; - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Headers for $url are " . Data::Dump::dump( $http->response->headers ) ); } @@ -293,11 +292,11 @@ sub readRemoteHeaders { if ( ref $type eq 'ARRAY' ) { $type = $type->[0]; } - + $type = Slim::Music::Info::mimeToType($type) || $type; - + # Handle some special cases - + # Bug 3396, some m4a audio is incorrectly served as audio/mpeg. # In this case, prefer the file extension to the content-type if ( $url =~ /aac$/i && ($type eq 'mp3' || $type eq 'txt') ) { @@ -312,33 +311,34 @@ sub readRemoteHeaders { elsif ( $type =~ /(?:htm|txt)/ && $url =~ /\.(asx|m3u|pls|wpl|wma)$/i ) { $type = $1; } - + # KWMR misconfiguration elsif ( $type eq 'wma' && $url =~ /\.(m3u)$/i ) { $type = $1; } - + # fall back to m3u for html and text elsif ( $type =~ /(?:htm|txt)/ ) { $type = 'm3u'; } - + # Some Shoutcast/Icecast servers don't send content-type if ( !$type && $http->response->header('icy-name') ) { $type = 'mp3'; } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "Content-type for $url detected as $type (" . $http->response->content_type . ")" ); } - + # Set content-type for original URL and redirected URL main::DEBUGLOG && $log->is_debug && $log->debug( 'Updating content-type for ' . $track->url . " to $type" ); + Slim::Schema->clearContentTypeCache( $track->url ); $track = Slim::Music::Info::setContentType( $track->url, $type ); - + if ( $track->url ne $url ) { my $update; - + # Don't create a duplicate object if the only difference is http:// instead of mms:// if ( $track->url =~ m{^mms://(.+)} ) { if ( $url ne "http://$1" ) { @@ -348,40 +348,40 @@ sub readRemoteHeaders { else { $update = 1; } - + if ( $update ) { main::DEBUGLOG && $log->is_debug && $log->debug( "Updating redirected URL $url" ); - + # Get/create a new entry for the redirected track my $redirTrack = Slim::Schema->updateOrCreate( { url => $url, } ); - + # Copy values from original track $redirTrack->title( $track->title ); $redirTrack->content_type( $track->content_type ); $redirTrack->bitrate( $track->bitrate ); - + $redirTrack->update; - + # Delete original track $track->delete; - + $track = $redirTrack; } } # Is this an audio stream or a playlist? - if ( Slim::Music::Info::isSong( $track, $type ) ) { + if ( $type = Slim::Music::Info::isSong( $track, $type ) ) { main::INFOLOG && $log->is_info && $log->info("This URL is an audio stream [$type]: " . $track->url); - + $track->content_type($type); - + if ( $type eq 'wma' ) { # WMA streams require extra processing, we must parse the Describe header info - + main::DEBUGLOG && $log->is_debug && $log->debug('Reading WMA header'); - + # If URL was http but content-type is wma, change URL if ( $track->url =~ /^http/i ) { # XXX: may create duplicate track entries @@ -400,9 +400,9 @@ sub readRemoteHeaders { } elsif ( $type eq 'aac' ) { # Bug 16379, AAC streams require extra processing to check for the samplerate - + main::DEBUGLOG && $log->is_debug && $log->debug('Reading AAC header'); - + $http->read_body( { readLimit => 4 * 1024, onBody => \&parseAACHeader, @@ -413,28 +413,49 @@ sub readRemoteHeaders { # Read the header to allow support for oggflac as it requires different decode path main::DEBUGLOG && $log->is_debug && $log->debug('Reading Ogg header'); - + $http->read_body( { readLimit => 64, onBody => \&parseOggHeader, passthrough => [ $track, $args ], } ); } + elsif ( $type eq 'wav' ) { + + # Read the header to allow support for wav as it requires different decode path + main::DEBUGLOG && $log->is_debug && $log->debug('Reading WAV header'); + $http->read_body( { + readLimit => 36, + onBody => \&parseWavHeader, + passthrough => [ $track, $args ], + } ); + } + elsif ( $type eq 'aif' ) { + + # Read the header to allow support for aif as it requires different decode path + main::DEBUGLOG && $log->is_debug && $log->debug('Reading AIF header'); + + $http->read_body( { + readLimit => 4*1024, + onBody => \&parseAifHeader, + passthrough => [ $track, $args ], + } ); + } else { # If URL was mms but content-type is not wma, change URL if ( $track->url =~ /^mms/i ) { main::DEBUGLOG && $log->is_debug && $log->debug("URL was mms:// but content-type is $type, fixing URL to http://"); - + # XXX: may create duplicate track entries my $httpURL = $track->url; $httpURL =~ s/^mms/http/i; $track->url( $httpURL ); $track->update; } - + my $bitrate; my $vbr = 0; - + # Look for Icecast info header and determine bitrate from this if ( my $audioinfo = $http->response->header('ice-audio-info') ) { ($bitrate) = $audioinfo =~ /ice-(?:bitrate|quality)=([^;]+)/i; @@ -444,42 +465,42 @@ sub readRemoteHeaders { my $quality = sprintf "%d", $1; $bitrate = $ogg_quality{$quality}; $vbr = 1; - + main::DEBUGLOG && $log->is_debug && $log->debug("Found bitrate from Ogg quality header: $bitrate"); } - else { + else { main::DEBUGLOG && $log->is_debug && $log->debug("Found bitrate from ice-audio-info header: $bitrate"); } } } - + # Look for bitrate information in header indicating it's an Icy stream - elsif ( $bitrate = ( $http->response->header('icy-br') || $http->response->header('x-audiocast-bitrate') ) * 1000 ) { + elsif ( $bitrate = ( $http->response->header('icy-br') || $http->response->header('x-audiocast-bitrate') || 0 ) * 1000 ) { main::DEBUGLOG && $log->is_debug && $log->debug("Found bitrate in Icy header: $bitrate"); } - + if ( $bitrate ) { if ( $bitrate < 1000 ) { $bitrate *= 1000; } - + Slim::Music::Info::setBitrate( $track, $bitrate, $vbr ); - + if ( $track->url ne $url ) { Slim::Music::Info::setBitrate( $url, $bitrate, $vbr ); } - + # We don't need to read any more data from this stream $http->disconnect; - + # All done - + # Bug 11001, if the URL uses basic authentication, it may be an Icecast # server that allows only 1 connection per user. Delay this callback for a second # to avoid the chance of getting a 401 error when trying to stream. - if ( $track->url =~ m{http://[^:]+:[^@]+@} ) { + if ( $track->url =~ m{https?://[^:]+:[^@]+@} ) { main::DEBUGLOG && $log->is_debug && $log->debug( 'Auth stream detected, waiting 1 second before streaming' ); - + Slim::Utils::Timers::setTimer( undef, Time::HiRes::time() + 1, @@ -493,11 +514,15 @@ sub readRemoteHeaders { } } else { - # We still need to read more info about this stream, but we can begin playing it now - $cb->( $track, undef, @{$pt} ); - + # XXX - for whatever reason we have to disconnect an https connection before we can do another connection... + # we'll start playback once the scanning has finished + if ( $track->url !~ /^https/ ) { + # We still need to read more info about this stream, but we can begin playing it now - unless it's an https stream + $cb->( $track, undef, @{$pt} ); + } + # Continue scanning in the background - + # We may be able to determine the bitrate or other tag information # about this remote stream/file by reading a bit of audio data main::DEBUGLOG && $log->is_debug && $log->debug('Reading audio data in the background to detect bitrate and/or tags'); @@ -512,7 +537,7 @@ sub readRemoteHeaders { } else { main::DEBUGLOG && $log->is_debug && $log->debug('This URL is a playlist: ' . $track->url); - + # Read the rest of the playlist $http->read_body( { readLimit => 128 * 1024, @@ -524,57 +549,57 @@ sub readRemoteHeaders { sub parseWMAHeader { my ( $http, $track, $args ) = @_; - + my $client = $args->{client}; my $cb = $args->{cb} || sub {}; my $pt = $args->{pt} || []; - + # Check for WMA chunking header from a server and remove it my $header = $http->response->content; my $chunkType = unpack 'v', substr( $header, 0, 2 ); if ( $chunkType == 0x4824 ) { substr $header, 0, 12, ''; } - + # The header may be at the front of the file, if the remote # WMA file is not a live stream my $fh = File::Temp->new(); $fh->write( $header, length($header) ); $fh->seek(0, 0); - + my $wma = Audio::Scan->scan_fh( asf => $fh ); - + if ( !$wma->{info}->{max_bitrate} ) { main::DEBUGLOG && $log->is_debug && $log->debug('Unable to parse WMA header'); - + # Delete bad item $track->delete; - + return $cb->( undef, 'ASF_UNABLE_TO_PARSE', @{$pt} ); } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( 'WMA header data for ' . $track->url . ': ' . Data::Dump::dump($wma) ); } - + my $streamNum = 1; - + # Some ASF streams appear to have no stream objects (mms://ms1.capitalinteractive.co.uk/fm_high) # I think it's safe to just assume stream #1 in this case if ( ref $wma->{info}->{streams} ) { - + # Look through all available streams and select the one with the highest bitrate still below # the user's preferred max bitrate my $max = preferences('server')->get('maxWMArate') || 9999; - + my $bitrate = 0; my $valid = 0; - + for my $stream ( @{ $wma->{info}->{streams} } ) { next unless defined $stream->{stream_number}; - + my $streamBitrate = sprintf "%d", $stream->{bitrate} / 1000; - + # If stream is ASF_Command_Media, it may contain metadata, so let's get it if ( $stream->{stream_type} eq 'ASF_Command_Media' ) { main::DEBUGLOG && $log->is_debug && $log->debug( "Possible ASF_Command_Media metadata stream: \#$stream->{stream_number}, $streamBitrate kbps" ); @@ -591,32 +616,32 @@ sub parseWMAHeader { || $stream->{codec_id} == 0x000a ); - + main::DEBUGLOG && $log->is_debug && $log->debug( "Available stream: \#$stream->{stream_number}, $streamBitrate kbps" ); if ( $stream->{bitrate} > $bitrate && $max >= $streamBitrate ) { $streamNum = $stream->{stream_number}; $bitrate = $stream->{bitrate}; } - + $valid++; } - + # If we saw no valid streams, such as a stream with only MP3 codec, give up if ( !$valid ) { main::DEBUGLOG && $log->is_debug && $log->debug('WMA contains no valid audio streams'); - + # Delete bad item $track->delete; - + return $cb->( undef, 'ASF_UNABLE_TO_PARSE', @{$pt} ); } - + if ( !$bitrate && ref $wma->{info}->{streams}->[0] ) { # maybe we couldn't parse bitrate information, so just use the first stream $streamNum = $wma->{info}->{streams}->[0]->{stream_number}; } - + if ( $bitrate ) { Slim::Music::Info::setBitrate( $track, $bitrate ); } @@ -628,44 +653,44 @@ sub parseWMAHeader { ) ); } } - + # Set duration if available (this is not a broadcast stream) - if ( my $ms = $wma->{info}->{song_length_ms} ) { + if ( my $ms = $wma->{info}->{song_length_ms} ) { Slim::Music::Info::setDuration( $track, int($ms / 1000) ); } - + # Save this metadata for the MMS protocol handler to use if ( my $song = $args->{song} ) { my $sd = $song->scanData(); if (!defined $sd) { $song->scanData($sd = {}); - } + } $sd->{$track->url} = { streamNum => $streamNum, metadata => $wma, headers => $http->response->headers, }; } - + # All done $cb->( $track, undef, @{$pt} ); } sub parseAACHeader { my ( $http, $track, $args ) = @_; - + my $client = $args->{client}; my $cb = $args->{cb} || sub {}; my $pt = $args->{pt} || []; - + my $header = $http->response->content; - + my $fh = File::Temp->new(); $fh->write( $header, length($header) ); $fh->seek(0, 0); - + my $aac = Audio::Scan->scan_fh( aac => $fh ); - + if ( my $samplerate = $aac->{info}->{samplerate} ) { if ( $samplerate <= 24000 ) { # XXX remove when Audio::Scan is updated to 0.84 $samplerate *= 2; @@ -673,14 +698,14 @@ sub parseAACHeader { $track->samplerate($samplerate); main::DEBUGLOG && $log->is_debug && $log->debug("AAC samplerate: $samplerate"); } - + # All done $cb->( $track, undef, @{$pt} ); } sub parseOggHeader { my ( $http, $track, $args ) = @_; - + my $client = $args->{client}; my $cb = $args->{cb} || sub {}; my $pt = $args->{pt} || []; @@ -695,17 +720,127 @@ sub parseOggHeader { Slim::Schema->clearContentTypeCache( $track->url ); Slim::Music::Info::setContentType( $track->url, 'ogf' ); $track->content_type('ogf'); + + my $samplerate = (unpack('N', substr($data, 26, 4)) & 0x00fffff0)>>4; + my $samplesize = ((unpack('n', substr($data, 29, 2)) & 0x01f0)>>4)+1; + my $channels = ((unpack('C', substr($data, 29, 1)) & 0x0e)>>1)+1; + my $bitrate = 0.6 * $samplerate * $samplesize * $channels; + $track->samplerate($samplerate); + $track->samplesize($samplesize); + $track->channels($channels); + Slim::Music::Info::setBitrate( $track->url, $bitrate ); + if ( main::DEBUGLOG && $log->is_debug ) { + $log->debug( sprintf( "OggFlac: %dHz, %dBits, %dch => estimated bitrate: %dkbps", + $samplerate, $samplesize, $channels, int( $bitrate / 1000 ) ) ); + } + # search for Ogg Opus header within the data - if so change the content type to opus for OggOpus + # OggOpus header defined: https://people.xiph.org/~giles/2013/draft-ietf-codec-oggopus.html#rfc.section.5.1 + } elsif (substr($data, 0, 8) eq 'OpusHead') { + main::DEBUGLOG && $log->is_debug && $log->debug("Ogg stream is OggOpus - setting content type [ops]"); + Slim::Schema->clearContentTypeCache( $track->url ); + Slim::Music::Info::setContentType( $track->url, 'ops' ); + $track->content_type('ops'); + + my $samplerate = unpack('V', substr($data, 12, 4)); + my $channels = unpack('C', substr($data, 9, 1)); + $track->samplerate($samplerate); + $track->samplesize(16); + if ( main::DEBUGLOG && $log->is_debug ) { + $log->debug( sprintf( "OggOpus: input %dHz, %dch", $samplerate, $channels ) ); + } } # All done $cb->( $track, undef, @{$pt} ); } +sub parseWavHeader { + my ( $http, $track, $args ) = @_; + + my $client = $args->{client}; + my $cb = $args->{cb} || sub {}; + my $pt = $args->{pt} || []; + + my $data = $http->response->content; + + # do minimum check + if (substr($data, 0, 4) ne 'RIFF') { + $cb->( $track, undef, @{$pt} ); + return; + } + + # search for Wav headers within the data + my $samplerate = unpack('V', substr($data, 24, 4)); + my $samplesize = unpack('v', substr($data, 34, 2)); + my $channels = unpack('v', substr($data, 22, 2)); + my $bitrate = $samplerate * $samplesize * $channels; + $track->samplerate($samplerate); + $track->samplesize($samplesize); + $track->channels($channels); + Slim::Music::Info::setBitrate( $track->url, $bitrate ); + if ( main::DEBUGLOG && $log->is_debug ) { + $log->debug( sprintf( "Wav: %dHz, %dBits, %dch => bitrate: %dkbps", + $samplerate, $samplesize, $channels, int( $bitrate / 1000 ) ) ); + } + + # All done + $cb->( $track, undef, @{$pt} ); +} + +sub parseAifHeader { + my ( $http, $track, $args ) = @_; + + my $client = $args->{client}; + my $cb = $args->{cb} || sub {}; + my $pt = $args->{pt} || []; + + my $data = $http->response->content; + + # do minimum check + if (substr($data, 0, 4) ne 'FORM') { + $cb->( $track, undef, @{$pt} ); + return; + } + + my $offset = 12; + + while ($offset < length($data) - 22) { + + if (substr($data, $offset, 4) eq 'COMM') { + my $samplesize = unpack('n', substr($data, $offset+14, 2)); + my $channels = unpack('n', substr($data, $offset+8, 2)); + # sample rate is encoded as IEEE 80 bit extended format + my $samplerate = unpack('N', substr($data, $offset+18, 4)); + my $exponent = (unpack('s>', substr($data, $offset+16, 2)) & 0x7fff) - 16383 - 31; + while ($exponent < 0) { $samplerate >>= 1; $exponent++; } + while ($exponent > 0) { $samplerate <<= 1; $exponent--; } + my $bitrate = $samplerate * $samplesize * $channels; + $track->samplerate($samplerate); + $track->samplesize($samplesize); + $track->channels($channels); + $track->endian(1); + Slim::Music::Info::setBitrate( $track->url, $bitrate ); + if ( main::DEBUGLOG && $log->is_debug ) { + $log->debug( sprintf( "Aif: %dHz, %dBits, %dch => bitrate: %dkbps", + $samplerate, $samplesize, $channels, int( $bitrate / 1000 ) ) ); + } + last; + } + + $offset += unpack('N', substr($data, $offset+4, 4)) + 8; + } + + $cb->( $track, undef, @{$pt} ); +} + sub streamAudioData { my ( $http, $dataref, $track, $args, $url ) = @_; - + + return 1 unless defined $$dataref; + + my $len = length($$dataref); my $first; - + # Buffer data to a temp file, 128K of data by default my $fh = $args->{_scanbuf}; if ( !$fh ) { @@ -715,10 +850,9 @@ sub streamAudioData { $first = 1; main::DEBUGLOG && $log->is_debug && $log->debug( $track->url . ' Buffering audio stream data to temp file ' . $fh->filename ); } - - my $len = length($$dataref); + $fh->write( $$dataref, $len ); - + if ( $first ) { if ( $$dataref =~ /^ID3/ ) { # get ID3v2 tag length from bytes 7-10 @@ -728,70 +862,68 @@ sub streamAudioData { for my $b ( unpack 'C4', $rawsize ) { $id3size = ($id3size << 7) + $b; } - + $id3size += 10; - + # Read the full ID3v2 tag + some audio frames for bitrate $args->{_scanlen} = $id3size + (16 * 1024); - + main::DEBUGLOG && $log->is_debug && $log->debug( 'ID3v2 tag detected, will read ' . $args->{_scanlen} . ' bytes' ); } - + # XXX: other tag types may need more than 128K too # Reset fh back to the end $fh->seek( 0, 2 ); } - + $args->{_scanlen} -= $len; - - if ( $args->{_scanlen} > 0 ) { + + if ( $args->{_scanlen} > 0 && $len) { # Read more data - #$log->is_debug && $log->debug( $track->url . ' Bytes left: ' . $args->{_scanlen} ); - return 1; } - + # Parse tags and bitrate my $bitrate = -1; my $vbr; - + my $cl = $http->response->content_length; my $type = $track->content_type; my $formatClass = Slim::Formats->classForFormat($type); - + if ( $formatClass && Slim::Formats->loadTagFormatForType($type) && $formatClass->can('scanBitrate') ) { ($bitrate, $vbr) = eval { $formatClass->scanBitrate( $fh, $track->url ) }; - + if ( $@ ) { $log->error("Unable to scan bitrate for " . $track->url . ": $@"); $bitrate = 0; } - + if ( $bitrate > 0 ) { Slim::Music::Info::setBitrate( $track, $bitrate, $vbr ); if ($cl) { Slim::Music::Info::setDuration( $track, ( $cl * 8 ) / $bitrate ); - } - + } + # Copy bitrate to redirected URL if ( $track->url ne $url ) { Slim::Music::Info::setBitrate( $url, $bitrate ); if ($cl) { Slim::Music::Info::setDuration( $url, ( $cl * 8 ) / $bitrate ); - } + } } } } else { main::DEBUGLOG && $log->is_debug && $log->debug("Unable to parse audio data for $type file"); } - + # Update filesize with Content-Length if ( $cl ) { $track->filesize( $cl ); $track->update; - + # Copy size to redirected URL if ( $track->url ne $url ) { my $redir = Slim::Schema->updateOrCreate( { @@ -801,70 +933,77 @@ sub streamAudioData { $redir->update; } } - + # Delete temp file and other data $fh->close; unlink $fh->filename if -e $fh->filename; delete $args->{_scanbuf}; delete $args->{_scanlen}; - + + # as https for whatever reason didn't allow us to start the stream while scanning + # we're now disconnecting to allow the stream to start + if ( $args->{cb} && $track->url =~ /^https/ ) { + $http->disconnect; + $args->{cb}->( $track, undef, @{$args->{pt} || []} ); + } + # Disconnect return 0; } sub parsePlaylist { my ( $http, $playlist, $args ) = @_; - + my $client = $args->{client}; my $cb = $args->{cb} || sub {}; my $pt = $args->{pt} || []; - + my @results; - + my $type = $playlist->content_type; - + my $formatClass = Slim::Formats->classForFormat($type); if ( $formatClass && Slim::Formats->loadTagFormatForType($type) && $formatClass->can('read') ) { my $fh = IO::String->new( $http->response->content_ref ); @results = eval { $formatClass->read( $fh, '', $playlist->url ) }; } - + if ( !scalar @results || !defined $results[0]) { main::DEBUGLOG && $log->is_debug && $log->debug( "Unable to parse playlist for content-type $type $@" ); - + # delete bad playlist $playlist->delete; - + return $cb->( undef, 'PLAYLIST_NO_ITEMS_FOUND', @{$pt} ); } - + # Convert the track to a playlist object $playlist = Slim::Schema->objectForUrl( { url => $playlist->url, playlist => 1, } ); - + # Link the found tracks with the playlist $playlist->setTracks( \@results ); - + if ( main::INFOLOG && $log->is_info ) { $log->info( 'Found ' . scalar( @results ) . ' items in playlist ' . $playlist->url ); main::DEBUGLOG && $log->debug( map { $_->url . "\n" } @results ); } - + # Scan all URLs in the playlist concurrently my $delay = 0; my $ready = 0; my $scanned = 0; my $total = scalar @results; - + for my $entry ( @results ) { if ( !blessed($entry) ) { $total--; next; } - + __PACKAGE__->scanURL( $entry->url, { client => $client, song => $args->{song}, @@ -873,12 +1012,12 @@ sub parsePlaylist { title => (($playlist->title && $playlist->title =~ /^(?:http|mms)/i) ? undef : $playlist->title), cb => sub { my ( $result, $error ) = @_; - + # Bug 10208: If resulting track is not the same as entry (due to redirect), # we need to adjust the playlist if ( blessed($result) && $result->id != $entry->id ) { main::DEBUGLOG && $log->is_debug && $log->debug('Scanned track changed, updating playlist'); - + my $i = 0; for my $e ( @results ) { if ( $e->id == $entry->id ) { @@ -887,51 +1026,51 @@ sub parsePlaylist { } $i++; } - + # Get the $playlist object again, as it may have changed $playlist = Slim::Schema->objectForUrl( { url => $playlist->url, playlist => 1, } ); - + $playlist->setTracks( \@results ); } - + $scanned++; - + main::DEBUGLOG && $log->is_debug && $log->debug("Scanned $scanned/$total items in playlist"); - + if ( !$ready ) { # As soon as we find an audio URL, start playing it and continue scanning the rest # of the playlist in the background if ( my $entry = $playlist->getNextEntry ) { - + if ( $entry->bitrate ) { # Copy bitrate to playlist Slim::Music::Info::setBitrate( $playlist->url, $entry->bitrate, $entry->vbr_scale ); } - + # Copy title if the playlist is untitled or a URL # If entry doesn't have a title either, use the playlist URL if ( !$playlist->title || $playlist->title =~ /^(?:http|mms)/i ) { $playlist = Slim::Music::Info::setTitle( $playlist->url, $entry->title || $playlist->url ); } - + main::DEBUGLOG && $log->is_debug && $log->debug('Found at least one audio URL in playlist'); - + $ready = 1; - + $cb->( $playlist, undef, @{$pt} ); } } - + if ( $scanned == $total ) { main::DEBUGLOG && $log->is_debug && $log->debug( 'Playlist scan of ' . $playlist->url . ' finished' ); - + # If we scanned everything and are still not ready, fail if ( !$ready ) { main::DEBUGLOG && $log->is_debug && $log->debug( 'No audio tracks found in playlist' ); - + # Get error of last item we tried in the playlist, or a generic error my $error; for my $track ( $playlist->tracks ) { @@ -939,20 +1078,20 @@ sub parsePlaylist { $error = $track->error; } } - + $error ||= 'PLAYLIST_NO_ITEMS_FOUND'; - + # Delete bad playlist $playlist->delete; - + $cb->( undef, $error, @{$pt} ); } } }, } ); - + # Stagger playlist scanning by a small amount so we prefer the first item - + # XXX: This can be a problem if a playlist file contains 'backup' streams or files # we would not want to play these if any of the real streams in the playlist are valid. $delay += 1; diff --git a/Slim/Utils/Scheduler.pm b/Slim/Utils/Scheduler.pm index ae731120195..77c1ab0bd00 100644 --- a/Slim/Utils/Scheduler.pm +++ b/Slim/Utils/Scheduler.pm @@ -1,8 +1,7 @@ package Slim::Utils::Scheduler; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/ServiceManager.pm b/Slim/Utils/ServiceManager.pm index 2f503856430..63c4d9bf8b9 100644 --- a/Slim/Utils/ServiceManager.pm +++ b/Slim/Utils/ServiceManager.pm @@ -1,6 +1,6 @@ package Slim::Utils::ServiceManager; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/ServiceManager/OSX.pm b/Slim/Utils/ServiceManager/OSX.pm index 03cd378a95f..4024d0faa37 100644 --- a/Slim/Utils/ServiceManager/OSX.pm +++ b/Slim/Utils/ServiceManager/OSX.pm @@ -1,6 +1,6 @@ package Slim::Utils::ServiceManager::OSX; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/ServiceManager/Win32.pm b/Slim/Utils/ServiceManager/Win32.pm index 45d66f607f4..33ae2838cdb 100644 --- a/Slim/Utils/ServiceManager/Win32.pm +++ b/Slim/Utils/ServiceManager/Win32.pm @@ -1,6 +1,6 @@ package Slim::Utils::ServiceManager::Win32; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/SoundCheck.pm b/Slim/Utils/SoundCheck.pm index 9459383bf6b..85e70f809ea 100644 --- a/Slim/Utils/SoundCheck.pm +++ b/Slim/Utils/SoundCheck.pm @@ -1,8 +1,7 @@ package Slim::Utils::SoundCheck; -# $Id$ # -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/Strings.pm b/Slim/Utils/Strings.pm index d39bbaa6063..7e981a8c756 100644 --- a/Slim/Utils/Strings.pm +++ b/Slim/Utils/Strings.pm @@ -1,8 +1,7 @@ package Slim::Utils::Strings; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -441,6 +440,7 @@ sub storeExtraStrings { $extraStringsCache = eval { from_json( read_file($extraCache) ) }; if ( $@ ) { + $log->error("Failed to read extrastrings.json file: $@"); $extraStringsCache = {}; } } @@ -467,6 +467,12 @@ sub storeExtraStrings { } if ( $extraStringsDirty ) { + # secondary languages need to be refreshed - remove the cached data + foreach (keys %{ languageOptions() }) { + next if $_ eq $currentLang; + delete $strings->{$_} if $strings->{$_}; + } + # Batch changes to avoid lots of writes Slim::Utils::Timers::killTimers( $extraCache, \&_writeExtraStrings ); Slim::Utils::Timers::setTimer( $extraCache, time() + 5, \&_writeExtraStrings ); @@ -480,6 +486,8 @@ sub _writeExtraStrings { $extraStringsDirty = 0; eval { write_file( $extraCache, to_json($extraStringsCache) ) }; + + $log->error("Failed to write extrastrings.json file: $@") if $@; }; =head2 loadExtraStrings diff --git a/Slim/Utils/Text.pm b/Slim/Utils/Text.pm index fcab996cda4..ab7fa31b840 100644 --- a/Slim/Utils/Text.pm +++ b/Slim/Utils/Text.pm @@ -3,9 +3,8 @@ package Slim::Utils::Text; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -136,8 +135,8 @@ sub ignoreCaseArticles { my $transliterate = shift; my $ignoreArticles = (shift) ? '1' : '0'; - if (!defined $s) { - return undef; + if (!$s) { + return $s; } # We don't handle references of any kind. diff --git a/Slim/Utils/Timers.pm b/Slim/Utils/Timers.pm index da00958d828..6c52963990c 100644 --- a/Slim/Utils/Timers.pm +++ b/Slim/Utils/Timers.pm @@ -1,8 +1,7 @@ package Slim::Utils::Timers; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/Unicode.pm b/Slim/Utils/Unicode.pm index 6321b8bd2e8..dae21e7ce72 100644 --- a/Slim/Utils/Unicode.pm +++ b/Slim/Utils/Unicode.pm @@ -1,6 +1,9 @@ package Slim::Utils::Unicode; -# $Id$ +# Logitech Media Server Copyright 2003-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. =head1 NAME diff --git a/Slim/Utils/Update.pm b/Slim/Utils/Update.pm index a9afb42d03a..fc9f0bf2852 100644 --- a/Slim/Utils/Update.pm +++ b/Slim/Utils/Update.pm @@ -28,7 +28,7 @@ my $versionFile; sub checkVersion { my $cb = shift; - + # clean up old download location Slim::Utils::Misc::deleteFiles($prefs->get('cachedir'), qr/^(?:Squeezebox|SqueezeCenter|LogitechMediaServer).*\.(pkg|dmg|exe)(\.tmp)?$/i); @@ -39,20 +39,20 @@ sub checkVersion { main::INFOLOG && $log->is_info && $log->info("We're running from the source - don't check for updates"); return; } - + return unless $prefs->get('checkVersion') || $cb; my $installer = getUpdateInstaller() || ''; - + # reset update download status in case our system is up to date if ( $installer && installerIsUpToDate($installer) ) { - + main::INFOLOG && $log->info("We're up to date (v$::VERSION, $::REVISION). Reset update notifiers."); - + $::newVersion = undef; setUpdateInstaller(); } - + $os->initUpdate() if $os->canAutoUpdate() && $prefs->get('autoDownloadUpdate'); my $lastTime = $prefs->get('checkVersionLastTime'); @@ -78,7 +78,7 @@ sub checkVersion { main::INFOLOG && $log->info("Checking version now."); my $url = main::NOMYSB ? (Slim::Networking::Repositories->getUrlForRepository('servers') . "$::VERSION/servers.xml") : (Slim::Networking::SqueezeNetwork->url('') . '/update/'); - + $url .= sprintf( "?version=%s&revision=%s&lang=%s&geturl=%s&os=%s&uuid=%s&pcount=%d", $::VERSION, @@ -89,18 +89,19 @@ sub checkVersion { $prefs->get('server_uuid'), Slim::Player::Client::clientCount(), ); - + main::DEBUGLOG && $log->debug("Using URL: $url"); - + my $params = { cb => $cb }; - - my $http = main::NOMYSB - ? Slim::Networking::SimpleAsyncHTTP->new(\&checkVersionCB, \&checkVersionError, $params) - : Slim::Networking::SqueezeNetwork->new(\&checkVersionCB, \&checkVersionError, $params); - - $http->get($url); + + if (main::NOMYSB) { + Slim::Networking::Repositories->get($url, \&checkVersionCB, \&checkVersionError, $params); + } + else { + Slim::Networking::SqueezeNetwork->new(\&checkVersionCB, \&checkVersionError, $params)->get($url); + } $prefs->set('checkVersionLastTime', Time::HiRes::time()); Slim::Utils::Timers::setTimer(0, Time::HiRes::time() + $prefs->get('checkVersionInterval'), \&checkVersion); @@ -117,22 +118,22 @@ sub checkVersionCB { if ($http->code =~ /^2\d\d/) { my $content = Slim::Utils::Unicode::utf8decode( $http->content() ); - + # Update checker logic is hosted on mysb.com. Once this is gone, we'll have to deal with it on our own. if (main::NOMYSB) { require XML::Simple; my $versions = XML::Simple::XMLin($content); - + my $osID = $os->installerOS() || 'default'; - + main::DEBUGLOG && $log->is_debug && $log->debug("Got list of installers:\n" . Data::Dump::dump($versions)); - + if ( my $update = $versions->{ $osID } ) { if ( $update->{version} && $update->{revision} ) { if ( Slim::Utils::Versions->compareVersions($update->{version}, $::VERSION) > 0 || $update->{revision} > $::REVISION ) { if ( $osID ne 'default' && $prefs->get('autoDownloadUpdate') ) { $version = $update->{url}; - + # prepend URL with our download host if we didn't get an absolute URL $version = Slim::Networking::Repositories->getUrlForRepository('servers') . $version unless $version =~ /^http/; } @@ -147,9 +148,9 @@ sub checkVersionCB { chomp($content); $version = $content; } - + $version ||= 0; - + main::DEBUGLOG && $log->debug($version || 'No new Logitech Media Server version available'); # reset the update flag @@ -157,11 +158,11 @@ sub checkVersionCB { # trigger download of the installer if available if ($version && $prefs->get('autoDownloadUpdate')) { - + main::INFOLOG && $log->info('Triggering automatic Logitech Media Server update download...'); getUpdate($version); } - + # if we got an update with download URL, display it in the web UI et al. elsif ($version && $version =~ /a href="downloads.slimdevices/i) { $::newVersion = $version; @@ -171,7 +172,7 @@ sub checkVersionCB { $::newVersion = 0; $log->warn(sprintf(Slim::Utils::Strings::string('CHECKVERSION_PROBLEM'), $http->code)); } - + $cb->($version) if $cb && ref $cb; } @@ -191,17 +192,17 @@ sub checkVersionError { # download the installer sub getUpdate { my $url = shift; - + my $params = $os->getUpdateParams($url); - + return unless $params; - + $params->{path} ||= scalar ( $os->dirsFor('updates') ); - + cleanup($params->{path}, 'tmp'); if ( $url && Slim::Music::Info::isURL($url) ) { - + main::INFOLOG && $log->info("URL to download update from: $url"); my ($a, $b, $file) = Slim::Utils::Misc::crackURL($url); @@ -210,7 +211,7 @@ sub getUpdate { # don't re-download if we're up to date if (installerIsUpToDate($file)) { main::INFOLOG && $log->info("We're up to date (v$::VERSION, $::REVISION). Reset update notifiers."); - + setUpdateInstaller(); return; } @@ -220,15 +221,15 @@ sub getUpdate { # don't re-download if file exists already if ( -e $file ) { main::INFOLOG && $log->info("We already have the latest installer file: $file"); - + setUpdateInstaller($file, $params->{cb}); return; } - + my $tmpFile = "$file.tmp"; setUpdateInstaller(); - + main::DEBUGLOG && $log->is_debug && $log->debug("Downloading...\n URL: $url\n Save as: $tmpFile\n Filename: $file"); # Save to a tmp file so we can check SHA @@ -241,7 +242,7 @@ sub getUpdate { params => $params, }, ); - + $download->get( $url ); } else { @@ -251,13 +252,13 @@ sub getUpdate { sub downloadAsyncDone { my $http = shift; - + my $file = $http->params('file'); my $tmpFile = $http->params('saveAs'); my $params = $http->params('params') || {}; - + my $path = $params->{'path'}; - + # make sure we got the file if (!-e $tmpFile) { $log->warn("Logitech Media Server installer download failed: file '$tmpFile' not stored on disk?!?"); @@ -275,7 +276,7 @@ sub downloadAsyncDone { main::INFOLOG && $log->is_info && $log->info("Successfully downloaded update installer file '$tmpFile'. Saving as $file"); unlink $file; my $success = rename $tmpFile, $file; - + if (-e $file) { setUpdateInstaller($file, $params->{cb}) ; } @@ -291,30 +292,30 @@ sub downloadAsyncDone { sub setUpdateInstaller { my ($file, $cb) = @_; - + $versionFile ||= getVersionFile(); - + if ($file && open(UPDATEFLAG, ">$versionFile")) { - + main::DEBUGLOG && $log->debug("Setting update version file to: $file"); - + print UPDATEFLAG $file; close UPDATEFLAG; if ($cb && ref($cb) eq 'CODE') { $cb->($file); } - + $::newVersion ||= string('SERVER_UPDATE_AVAILABLE_SHORT'); } - + elsif ($file) { - + $log->warn("Unable to update version file: $versionFile"); } - + else { - + unlink $versionFile; } } @@ -326,40 +327,40 @@ sub getVersionFile { sub getUpdateInstaller { - + return unless $prefs->get('autoDownloadUpdate'); - + $versionFile ||= getVersionFile(); - + main::DEBUGLOG && $log->is_debug && $log->debug("Reading update installer path from $versionFile"); - + open(UPDATEFLAG, $versionFile) || do { main::DEBUGLOG && $log->is_debug && $log->debug("No '$versionFile' available."); return ''; }; - + my $updateInstaller = ''; - + local $_; while ( ) { chomp; - + if (/(?:LogitechMediaServer|Squeezebox|SqueezeCenter).*/) { $updateInstaller = $_; last; } } - + close UPDATEFLAG; - + main::DEBUGLOG && $log->debug("Found update installer path: '$updateInstaller'"); - + return $updateInstaller; } sub installerIsUpToDate { - + return unless $prefs->get('autoDownloadUpdate'); my $installer = shift || ''; diff --git a/Slim/Utils/Validate.pm b/Slim/Utils/Validate.pm index 8a962854076..853a1011360 100644 --- a/Slim/Utils/Validate.pm +++ b/Slim/Utils/Validate.pm @@ -1,8 +1,6 @@ package Slim::Utils::Validate; -# $Id: Validate.pm 11972 2007-05-12 07:32:19Z kdf $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Utils/Versions.pm b/Slim/Utils/Versions.pm index f0480d96d29..8a3fa392795 100644 --- a/Slim/Utils/Versions.pm +++ b/Slim/Utils/Versions.pm @@ -1,8 +1,7 @@ package Slim::Utils::Versions; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License, version 2. diff --git a/Slim/Web/Cometd.pm b/Slim/Web/Cometd.pm index b9398ad466a..6a4c313e288 100644 --- a/Slim/Web/Cometd.pm +++ b/Slim/Web/Cometd.pm @@ -1,8 +1,7 @@ package Slim::Web::Cometd; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -410,22 +409,22 @@ sub handler { if ( $result->{error} ) { - my %error = { + my $error = { channel => '/slim/subscribe', error => $result->{error}, id => $id, }; - push @errors, \%error; + push @errors, $error; if ($result->{'errorNeedClient'}) { $log->error('errorNeedsClient: ', join(', ', $request->[0], @{$request->[1]})); # Force reconnect because client not connected. - # We should not need to force a new handshake, just a reconect. + # We should not need to force a new handshake, just a reconnect. # Any successful subscribes will have the acknowledgements in the $events queue # and others will be retried by the client unpon reconnect. # Let the client pick the interval to give SlimProto a chance to reconnect. - $error{'advice'} = { + $error->{'advice'} = { reconnect => 'retry', }; diff --git a/Slim/Web/Cometd/Manager.pm b/Slim/Web/Cometd/Manager.pm index ca4c85f76cf..63578a65f42 100644 --- a/Slim/Web/Cometd/Manager.pm +++ b/Slim/Web/Cometd/Manager.pm @@ -1,8 +1,7 @@ package Slim::Web::Cometd::Manager; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Graphics.pm b/Slim/Web/Graphics.pm index 91e42f48213..1bf17735556 100644 --- a/Slim/Web/Graphics.pm +++ b/Slim/Web/Graphics.pm @@ -409,6 +409,7 @@ sub artworkRequest { $body = \''; } + main::INFOLOG && $isInfo && $log->info(" Done Resizing: $fullpath using spec $spec"); $callback->( $client, $params, $body, @args ); }; diff --git a/Slim/Web/HTTP.pm b/Slim/Web/HTTP.pm index 75f17f2225f..11b21c4a423 100644 --- a/Slim/Web/HTTP.pm +++ b/Slim/Web/HTTP.pm @@ -1,10 +1,9 @@ package Slim::Web::HTTP; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -35,6 +34,7 @@ use MIME::QuotedPrint; use Scalar::Util qw(blessed); use Socket qw(:crlf SOMAXCONN SOL_SOCKET SO_SNDBUF inet_ntoa); use Storable qw(thaw); +use URI; use Slim::Networking::Select; use Slim::Player::HTTP; @@ -58,7 +58,7 @@ use constant HALFYEAR => 60 * 60 * 24 * 180; use constant METADATAINTERVAL => 32768; use constant MAXCHUNKSIZE => 32768; -# This used to be 0.05s but the CPU load associated with such fast retries is +# This used to be 0.05s but the CPU load associated with such fast retries is # really noticeable when playing remote streams. I guess that it is possible # that certain combinations of pipe buffers in a transcoding pipeline # might get caught by this but I have not been able to think of any - Alan. @@ -107,13 +107,13 @@ sub init { require Slim::Web::Template::NoWeb; $skinMgr = Slim::Web::Template::NoWeb->new(); } - + # Initialize graphics resizing Slim::Web::Graphics::init(); - + # Initialize JSON RPC Slim::Web::JSONRPC::init(); - + # Initialize Cometd Slim::Web::Cometd::init(); } @@ -137,12 +137,12 @@ sub openport { my %tested; my $testSocket; - + # start our listener foreach my $port ($listenerport, 9000..9010, 9100, 8000, 10000) { - + next if $tested{$port}; - + $openedport = $port; $tested{$port} = 1; @@ -152,7 +152,7 @@ sub openport { { $testSocket->close; } - + else { $http_server_socket = HTTP::Daemon->new( @@ -164,22 +164,22 @@ sub openport { Timeout => 0.001, ) and last; } - + $log->error("Can't setup the listening port $port for the HTTP server: $!"); } - + # if none of our ports could be opened, we'll have to give up if (!$http_server_socket) { - + $log->logdie("Running out of good ideas for the listening port for the HTTP server - giving up."); } - + defined(Slim::Utils::Network::blocking($http_server_socket,0)) || $log->logdie("Cannot set port nonblocking"); Slim::Networking::Select::addRead($http_server_socket, \&acceptHTTP); main::INFOLOG && $log->info("Server $0 accepting http connections on port $openedport"); - + if ($openedport != $listenerport) { $log->error("Previously configured port $listenerport was busy - we're now using port $openedport instead"); @@ -191,7 +191,7 @@ sub openport { $prefs->set('httpport', $openedport) ; } - + if ( $listeneraddr ) { $prefs->set( httpaddr => $listeneraddr ); } @@ -237,9 +237,9 @@ sub acceptHTTP { }; defined(Slim::Utils::Network::blocking($httpClient,0)) || $log->logdie("Cannot set port nonblocking"); - + binmode($httpClient); - + my $peer = $httpClient->peeraddr(); if ($httpClient->connected() && $peer) { @@ -249,7 +249,7 @@ sub acceptHTTP { # Check if source address is valid if (!($prefs->get('filterHosts')) || (Slim::Utils::Network::isAllowedHost($peer))) { - + # Timeout for reads from the client. HTTP::Daemon in get_request # will call select(,,,10) but should not block long # as we already know the socket is ready for reading @@ -287,7 +287,7 @@ sub skins { # Handle an HTTP request sub processHTTP { my $httpClient = shift || return; - + my $isDebug = ( main::DEBUGLOG && $log->is_debug ) ? 1 : 0; ### OLD ORDER ### @@ -317,11 +317,11 @@ sub processHTTP { ## Icy-MetaData (write sendMetaData) ## Parse URI (write $params) ## Skins (write params & path, redirected if nok) - ## More CSRF mgmt (looks at the modified path) + ## More CSRF mgmt (looks at the modified path) ## Log processed headers # else ## Send bad request - + # Store the time we started processing this request $httpClient->start_time( Time::HiRes::time() ); @@ -335,7 +335,7 @@ sub processHTTP { if (!defined $request) { my $reason = $httpClient->reason || 'unknown error reading request'; - + if ( main::INFOLOG && $log->is_info ) { $log->info("Client at $peeraddr{$httpClient}:" . $httpClient->peerport . " disconnected. ($reason)"); } @@ -343,7 +343,7 @@ sub processHTTP { closeHTTPSocket($httpClient, 0, $reason); return; } - + if ( main::INFOLOG && $log->is_info ) { $log->info( @@ -404,7 +404,7 @@ sub processHTTP { } else { - # If the client requests a close or a keep-alive, + # If the client requests a close or a keep-alive, # set the initial response to the same. $response->header('Connection' => $request->header('Connection')); @@ -431,7 +431,7 @@ sub processHTTP { } } - + if ( $keepAlives{$httpClient} ) { # set the keep-alive timeout Slim::Utils::Timers::setTimer( @@ -448,9 +448,11 @@ sub processHTTP { # the path is modified below for skins and stuff my $uri = $request->uri(); my $path = $uri->path(); - + main::DEBUGLOG && $isDebug && $log->debug("Raw path is [$path]"); + checkCORS($request, $response); + # break here for raw HTTP code # we hand the $response object only, it contains the almost unmodified request # we took care above of basic HTTP stuff and authorization @@ -460,7 +462,7 @@ sub processHTTP { main::DEBUGLOG && $isDebug && $log->info("Handling [$path] using raw function"); if (ref($rawFunc) eq 'CODE') { - + # XXX: should this use eval? &{$rawFunc}($httpClient, $response); return; @@ -469,13 +471,13 @@ sub processHTTP { # Set the request time - for If-Modified-Since $request->client_date(time()); - + my $csrfProtectionLevel = main::WEBUI && $prefs->get('csrfProtectionLevel'); - + if ( main::WEBUI && $csrfProtectionLevel ) { # remove our special X-Slim-CSRF header if present $request->remove_header("X-Slim-CSRF"); - + # store CSRF auth code in fake request header if present if ( defined($uri) && ($uri =~ m|^(.*)\;cauth\=([0-9a-f]{32})$| ) ) { @@ -493,22 +495,27 @@ sub processHTTP { $request->push_header("X-Slim-CSRF",$csrfAuth); } } - + # Dont' process cookies for graphics, stylesheets etc. if ($path && $path !~ m/(?:gif|png|jpe?g|css)$/i && $path !~ m{^/(?:music/[a-f\d]+/cover|imageproxy/.*/image)} ) { if ( my $cookie = $request->header('Cookie') ) { $params->{'cookies'} = { CGI::Cookie->parse($cookie) }; + + # define the precacheHiDPIArtwork pref if we're dealing with a high DPI monitor + if ( $params->{'cookies'}->{'Squeezebox-enableHiDPI'} && $params->{'cookies'}->{'Squeezebox-enableHiDPI'}->value > 1 && !defined $prefs->get('precacheHiDPIArtwork') ) { + $prefs->set('precacheHiDPIArtwork', 1); + } } } - + # Icy-MetaData $sendMetaData{$httpClient} = 0; - + if ($request->header('Icy-MetaData')) { $sendMetaData{$httpClient} = 1; } - # parse out URI + # parse out URI my $query = ($request->method() eq "POST") ? $request->content() : $uri->query(); $params->{url_query} = $query; @@ -518,7 +525,7 @@ sub processHTTP { my ($queryWithArgs, $queryToTest, $providedPageAntiCSRFToken); # CSRF: make list of params passed by HTTP client my %csrfReqParams; - + # XXX - unfortunately Logitech Media Server uses a query form # that can have a key without a value, yet it's # differnet from a key with an empty value. So we have @@ -585,18 +592,18 @@ sub processHTTP { } if ( main::WEBUI && $csrfProtectionLevel ) { - # for CSRF protection, get the query args in one neat string that + # for CSRF protection, get the query args in one neat string that # looks like a GET querystring value; this should handle GET and POST # equally well, only looking at the data that we would act on ($queryWithArgs, $queryToTest) = Slim::Web::HTTP::CSRF->getQueries($request, \%csrfReqParams); - + # Stash CSRF token in $params for use in TT templates $providedPageAntiCSRFToken = $params->{pageAntiCSRFToken}; # pageAntiCSRFToken is a bare token $params->{pageAntiCSRFToken} = Slim::Web::HTTP::CSRF->makePageToken($request); } - # Skins + # Skins if ($path) { $params->{'webroot'} = '/'; @@ -627,7 +634,7 @@ sub processHTTP { main::DEBUGLOG && $isDebug && $log->info("Alternate skin $desiredskin requested"); my $skinname = $skinMgr->isaSkin($desiredskin); - + if ($skinname) { main::DEBUGLOG && $isDebug && $log->info("Rendering using $skinname"); @@ -641,10 +648,10 @@ sub processHTTP { } else { # we can either throw a 404 here or just ignore the requested skin - + # ignore: commented out # $path =~ s{^/.+?/}{/}; - + # throw 404 $params->{'suggestion'} = qq(There is no "$desiredskin") . qq( skin, try ) . Slim::Utils::Network::serverURL() . qq( instead.); @@ -652,12 +659,12 @@ sub processHTTP { if ( $log->is_warn ) { $log->warn("Invalid skin requested: [" . join(' ', ($request->method, $request->uri)) . "]"); } - + $response->code(RC_NOT_FOUND); $response->content_type('text/html'); $response->header('Connection' => 'close'); $response->content_ref(filltemplatefile('html/errors/404.html', $params)); - + $httpClient->send_response($response); closeHTTPSocket($httpClient); return; @@ -674,7 +681,7 @@ sub processHTTP { return; } } - + if ( main::DEBUGLOG && $isDebug ) { $log->debug("Processed request headers: [\n" . $request->as_string() . "]"); } @@ -683,16 +690,23 @@ sub processHTTP { processURL($httpClient, $response, $params); } else { - - if ( $log->is_warn ) { - $log->warn("Bad Request: [" . join(' ', ($request->method, $request->uri)) . "]"); + if ( $request->method eq 'OPTIONS' ) { + checkCORS($request, $response); + $response->code(RC_OK); } - $response->code(RC_METHOD_NOT_ALLOWED); $response->header('Connection' => 'close'); - $response->content_type('text/html'); - $response->content_ref(filltemplatefile('html/errors/405.html', $params)); + if ( !$response->code() ) { + if ( $log->is_warn ) { + $log->warn("Bad Request: [" . join(' ', ($request->method, $request->uri)) . "]"); + } + + $response->code(RC_METHOD_NOT_ALLOWED); + $response->content_type('text/html'); + $response->content_ref(filltemplatefile('html/errors/405.html', $params)); + } + $httpClient->send_response($response); closeHTTPSocket($httpClient); } @@ -703,7 +717,7 @@ sub processHTTP { $response->content(""); $log->debug("Response Headers: [\n" . $response->as_string . "]"); } - + if ( main::DEBUGLOG && $isDebug ) { $log->debug( "End request: keepAlive: [" . @@ -724,8 +738,8 @@ sub processURL { # Command parameters are query parameters named p0 through pN # For example: - # http://host/status.m3u?p0=playlist&p1=jump&p2=2 - # http://host/status.m3u?command=playlist&subcommand=jump&p2=2 + # http://host/status.m3u?p0=playlist&p1=jump&p2=2 + # http://host/status.m3u?command=playlist&subcommand=jump&p2=2 # This example jumps to the second song in the playlist and sends a playlist as the response # # If there are multiple players, then they are specified by the player id @@ -738,7 +752,7 @@ sub processURL { } # This is trumped by query parameters 'command' and 'subcommand'. - # These are passed as the first two command parameters (p0 and p1), + # These are passed as the first two command parameters (p0 and p1), # while the rest of the query parameters are passed as third (p3). if (defined $params->{'command'} && $path !~ /^memoryusage/) { $p[0] = $params->{'command'}; @@ -753,7 +767,7 @@ sub processURL { # explicitly specified player (for web browsers or squeezeboxen) if (defined($params->{"player"})) { $client = Slim::Player::Client::getClient($params->{"player"}); - + if ( blessed($client) ) { if ( $path =~ m|plugins/UPnP/|i ) { # Bug 18053 - we're ignoring upnp requests, as these aren't our clients being active but whatever else @@ -768,14 +782,14 @@ sub processURL { # is this an HTTP stream? if (!defined($client) && ($path =~ /(?:stream\.mp3|stream)$/)) { - + # Bug 14825, allow multiple stream.mp3 clients from the same address with a player param my $address = $params->{player} || $peeraddr{$httpClient}; - + main::INFOLOG && $log->is_info && $log->info("processURL found HTTP client at address=$address"); - + $client = Slim::Player::Client::getClient($address); - + if (!defined($client)) { my $paddr = getpeername($httpClient); @@ -785,7 +799,7 @@ sub processURL { if ($paddr) { $client = Slim::Player::HTTP->new($address, $paddr, $httpClient); $client->init(); - + # Give the streaming player a descriptive name such as "Winamp from x.x.x.x" if ( $params->{userAgent} ) { my ($agent) = $params->{userAgent} =~ m{([^/]+)}; @@ -795,10 +809,10 @@ sub processURL { elsif ( $agent eq 'WinampMPEG' ) { $agent = 'Winamp'; } - + $client->name( $agent . ' ' . string('FROM') . ' ' . $address ); } - + # Bug 4795 # If the player has an existing playlist, start playing it without # requiring the user to press Play in the web UI @@ -830,7 +844,7 @@ sub processURL { $prefs->client($client)->set('transcodeBitrate',undef); } } - + # player specified from cookie if ( !defined $client && $params->{'cookies'} ) { if ( my $player = $params->{'cookies'}->{'Squeezebox-player'} ) { @@ -935,7 +949,7 @@ sub generateHTTPResponse { if ( Slim::Web::Pages->isRawDownload($path) ) { $contentType = 'application/octet-stream'; } - + if ( $path =~ /(?:music|video|image)\/[0-9a-f]+\/(?:download|cover)/ || $path =~ /^imageproxy\// ) { # Avoid generating templates for download URLs $contentType = 'application/octet-stream'; @@ -963,15 +977,37 @@ sub generateHTTPResponse { ); } + my $classOrCode = Slim::Web::Pages->getPageFunction($path); + + # protect access to settings pages: only allow from local network + if ( main::WEBUI + && !Slim::Utils::Network::ip_is_host($peeraddr{$httpClient}) + && $prefs->get('protectSettings') && !$prefs->get('authorize') + && $classOrCode && !ref $classOrCode && $classOrCode->isa('Slim::Web::Settings') + && ( Slim::Utils::Network::ip_is_gateway($peeraddr{$httpClient}) || Slim::Utils::Network::ip_on_different_network($peeraddr{$httpClient}) ) + ) { + my $hostIP = Slim::Utils::IPDetect::IP(); + $log->error("Access to settings pages is restricted to the local network or localhost: $peeraddr{$httpClient} -> $hostIP ($path)"); + + $response->code(RC_FORBIDDEN); + + $body = filltemplatefile('html/errors/403.html', $params); + + return prepareResponseForSending( + $client, + $params, + $body, + $httpClient, + $response, + ); + } + main::INFOLOG && $log->is_info && $log->info("Generating response for ($type, $contentType) $path"); - # some generally useful form details... - my $classOrCode = Slim::Web::Pages->getPageFunction($path); - if (defined($client) && $classOrCode) { $params->{'player'} = $client->id(); $params->{'myClientState'} = $client; - + if ( $path !~ m{(?:^progress\.|settings/)} ) { # save the player id in a cookie my $cookie = CGI::Cookie->new( @@ -985,14 +1021,14 @@ sub generateHTTPResponse { # this might do well to break up into methods if ($contentType =~ /(?:image|javascript|css)/ || $path =~ /html\//) { - + my $max = 60 * 60; - + # increase expiry to a week for static content, but not cover art unless ($contentType =~ /image/ && $path !~ /html\//) { $max = $max * 24 * 7; } - + # static content should expire from cache in one hour $response->expires( time() + $max ); $response->header('Cache-Control' => 'max-age=' . $max); @@ -1015,7 +1051,7 @@ sub generateHTTPResponse { delete $params->{'params'}; } - + # Static files handled here, stream them out to the browser to avoid wasting memory my $isStatic = 0; if ( $path =~ /favicon\.ico/ ) { @@ -1035,7 +1071,7 @@ sub generateHTTPResponse { $isStatic = 1; } } - + if ( $isStatic ) { ($mtime, $inode, $size) = getFileInfoForStaticContent($path, $params); @@ -1056,7 +1092,7 @@ sub generateHTTPResponse { # if we match one of the page functions as defined above, # execute that, and hand it a callback to send the data. - + $params->{'imageproxy'} = Slim::Networking::SqueezeNetwork->url( "/public/imageproxy" ) if !main::NOMYSB; @@ -1078,7 +1114,7 @@ sub generateHTTPResponse { } elsif ($classOrCode->can('handler')) { # Pull the player ID out and create a client from it - # if we need to use it for player settings. + # if we need to use it for player settings. if (exists $params->{'playerid'} && $classOrCode->needsClient) { $client = Slim::Player::Client::getClient($params->{'playerid'}); @@ -1092,7 +1128,7 @@ sub generateHTTPResponse { $response, ); } - + main::PERFMON && $startTime && Slim::Utils::PerfMon->check('web', AnyEvent->time - $startTime, "Page: $path"); } elsif ($path =~ /^(?:stream\.mp3|stream)$/o) { @@ -1109,7 +1145,7 @@ sub generateHTTPResponse { $response->header("icy-metaint" => METADATAINTERVAL); $response->header("icy-name" => string('WELCOME_TO_SQUEEZEBOX_SERVER')); } - + main::INFOLOG && $log->is_info && $log->info("Disabling keep-alive for stream.mp3"); delete $keepAlives{$httpClient}; Slim::Utils::Timers::killTimers( $httpClient, \&closeHTTPSocket ); @@ -1118,7 +1154,7 @@ sub generateHTTPResponse { my $headers = _stringifyHeaders($response) . $CRLF; $metaDataBytes{$httpClient} = - length($headers); - + addStreamingResponse($httpClient, $headers); return 0; @@ -1132,8 +1168,8 @@ sub generateHTTPResponse { ) { main::PERFMON && (my $startTime = AnyEvent->time); - - # Bug 15723, We need to track if we have an async artwork request so + + # Bug 15723, We need to track if we have an async artwork request so # we don't return data out of order my $async = 0; my $sentResponse = 0; @@ -1145,7 +1181,7 @@ sub generateHTTPResponse { sub { $sentResponse = 1; prepareResponseForSending(@_); - + if ( $async ) { main::INFOLOG && $log->is_info && $log->info('Async artwork request done, enable read'); Slim::Networking::Select::addRead($httpClient, \&processHTTP); @@ -1154,16 +1190,16 @@ sub generateHTTPResponse { $httpClient, $response, ); - + # If artworkRequest did not directly call the callback, we are in an async request if ( !$sentResponse ) { main::INFOLOG && $log->is_info && $log->info('Async artwork request pending, pause read'); Slim::Networking::Select::removeRead($httpClient); $async = 1; } - + main::PERFMON && $startTime && Slim::Utils::PerfMon->check('web', AnyEvent->time - $startTime, "Page: $path"); - + return; # return quickly with a 404 if web UI is disabled @@ -1173,9 +1209,9 @@ sub generateHTTPResponse { ) ) { $response->content_type('text/html'); $response->code(RC_NOT_FOUND); - + $$body = "

404 Not Found: $path

Logitech Media Server web UI is not available in --noweb mode.

"; - + return prepareResponseForSending( $client, $params, @@ -1186,13 +1222,13 @@ sub generateHTTPResponse { } elsif ($path =~ /music\/(-[0-9a-f]+)\/download/) { my $obj = Slim::Schema->find('Track', $1); - + # our download code can't handle the volatile files directly - let's fake a local file here if (blessed($obj) && Slim::Music::Info::isVolatile($obj->url)) { my $handler = Slim::Player::ProtocolHandlers->handlerForURL($obj->url); - + my $url = Slim::Utils::Misc::fileURLFromPath($handler->pathFromFileURL($obj->url)); - + my $tmpObj = Slim::Schema::RemoteTrack->updateOrCreate($url, { content_type => Slim::Music::Info::typeFromPath($url) }); @@ -1225,14 +1261,14 @@ sub generateHTTPResponse { } elsif ($path =~ /(?:music|video|image)\/([0-9a-f]+)\/download/) { # Bug 10730 my $id = $1; - + if ( $path =~ /music|video/ ) { main::INFOLOG && $log->is_info && $log->info("Disabling keep-alive for large file download"); delete $keepAlives{$httpClient}; Slim::Utils::Timers::killTimers( $httpClient, \&closeHTTPSocket ); $response->header( Connection => 'close' ); } - + # Reject bad getContentFeatures requests (DLNA 7.4.26.5) if ( my $gcf = $response->request->header('getContentFeatures.dlna.org') ) { if ( $gcf ne '1' ) { @@ -1263,12 +1299,17 @@ sub generateHTTPResponse { } elsif ($path =~ /(server|scanner|perfmon|log)\.(?:log|txt)/) { if ( main::WEBUI ) { - ($contentType, $body) = Slim::Web::Pages::Common->logFile($httpClient, $params, $response, $1); - - # when the full file is requested, then all the streaming is handled in the logFile call. Nothing is returned. - return 0 unless $contentType; + $body = Slim::Web::Pages::Common->logFile($httpClient, $params, $response, $1); + + if ($body) { + $contentType = 'text/html'; + } + else { + # when the full file is requested, then all the streaming is handled in the logFile call. Nothing is returned. + return 0 unless $contentType; + } } - + } elsif ($path =~ /status\.m3u/) { if ( main::WEBUI ) { @@ -1288,7 +1329,7 @@ sub generateHTTPResponse { } } elsif ( Slim::Web::Pages->isRawDownload($path) ) { - + # path is for download of known file outside http directory my ($file, $ct); @@ -1319,7 +1360,7 @@ sub generateHTTPResponse { Slim::Utils::Timers::killTimers( $httpClient, \&closeHTTPSocket ); $response->header( Connection => 'close' ); } - + # download the file main::INFOLOG && $log->is_info && $log->info("serving file: $file for path: $path"); sendStreamingFile( $httpClient, $response, $ct, $file ); @@ -1342,11 +1383,11 @@ sub generateHTTPResponse { $response, ); } - + } elsif ( $path =~ /anyurl/ ) { main::DEBUGLOG && $log->is_debug && $log->debug('anyurl - parameters processed, redirect to status page if needed'); $body = filltemplatefile('xmlbrowser_redirect.html', $params); - + } else { # who knows why we're here, we just know that something ain't right $$body = undef; @@ -1402,7 +1443,7 @@ sub generateHTTPResponse { # treat js.html differently - need the html ending to have it processed by TT, # but browser should consider it javascript - if ( $path =~ /js(?:-browse)?\.html/i) { + if ( $path =~ /js(?:|-\S*)\.html/i ) { $contentType = 'application/x-javascript'; } @@ -1413,10 +1454,10 @@ sub generateHTTPResponse { #} return 0 unless $body; - + if ( ref $body eq 'FileHandle' ) { $response->content_length( $size ); - + my $headers = _stringifyHeaders($response) . $CRLF; $streamingFiles{$httpClient} = $body; @@ -1426,7 +1467,7 @@ sub generateHTTPResponse { delete $peerclient{$httpClient}; addStreamingResponse($httpClient, $headers); - + return; } @@ -1437,31 +1478,31 @@ sub generateHTTPResponse { sub sendStreamingFile { my ( $httpClient, $response, $contentType, $file, $objOrHash, $showInBrowser ) = @_; - + # Send the file down - and hint to the browser # the correct filename to save it as. my $size = -s $file; - + $response->content_type( $contentType ); $response->content_length( $size ); $response->header('Content-Disposition', sprintf('attachment; filename="%s"', Slim::Utils::Misc::unescape(basename($file))) ) unless $showInBrowser; - + my $fh = FileHandle->new($file); - + # Range/TimeSeekRange my $range = $response->request->header('Range'); my $tsrange = $response->request->header('TimeSeekRange.dlna.org'); - + # If a Range is already provided, ignore TimeSeekRange my $isTSR; if ( $tsrange && !$range ) { # Translate TimeSeekRange into byte range my $valid = 0; - + my $formatClass = blessed($objOrHash) ? Slim::Formats->classForFormat($objOrHash->content_type) : undef; - + # Ignore TimeSeekRange unless we have a valid format class (currently this only supports audio) if ( $formatClass && Slim::Formats->loadTagFormatForType($objOrHash->content_type) && $formatClass->can('findFrameBoundaries') ) { # Valid is: npt=(start time)-(end time) @@ -1470,30 +1511,30 @@ sub sendStreamingFile { if ( $tsrange =~ /^npt=([^-]+)-([^\s]*)$/ ) { my $start = $1 || 0; my $end = $2; - + my $startbytes = 0; my $endbytes = $size - 1; - + if ( $start =~ /:/ ) { my ($h, $m, $s) = split /:/, $start; $start = ($h * 3600) + ($m * 60) + $s; } - + if ( $start > 0 ) { $startbytes = $formatClass->findFrameBoundaries($fh, undef, $start); main::DEBUGLOG && $log->is_debug && $log->debug("TimeSeekRange.dlna.org: Found start byte offset $startbytes for time $start"); } - + if ( $end ) { if ( $end =~ /:/ ) { my ($h, $m, $s) = split /:/, $end; $end = ($h * 3600) + ($m * 60) + $s; } - + $endbytes = $formatClass->findFrameBoundaries($fh, undef, $end); main::DEBUGLOG && $log->is_debug && $log->debug("TimeSeekRange.dlna.org: Found end offset $endbytes for time $end"); } - + if ( $startbytes == -1 && $endbytes == -1 ) { # DLNA 7.4.40.8, a valid time range syntax but out of range for the media $response->code(416); @@ -1504,17 +1545,17 @@ sub sendStreamingFile { $endbytes = $size - 1; } } - + if ( $startbytes >= 0 && $endbytes >= 0 ) { # Create a valid Range request, which will be handled by the below range code $range = "bytes=${startbytes}-${endbytes}"; $isTSR = 1; $valid = 1; - + my $duration = $objOrHash->secs; $end ||= $duration; $response->header( 'TimeSeekRange.dlna.org' => "npt=${start}-${end}/${duration} bytes=${startbytes}-${endbytes}/${size}" ); - + # If npt is "0-" don't perform a range request if ($start == 0 && $end == $duration) { $range = undef; @@ -1526,7 +1567,7 @@ sub sendStreamingFile { $response->code(400); } } - + if ( !$valid ) { $log->warn("Invalid TimeSeekRange.dlna.org request: $tsrange"); $response->code(406) unless $response->code >= 400; @@ -1536,7 +1577,7 @@ sub sendStreamingFile { return; } } - + # Support Range requests if ( $range ) { # Only support a single range request, and no support for suffix requests @@ -1544,7 +1585,7 @@ sub sendStreamingFile { my $first = $1 || 0; my $last = $2 || $size - 1; my $total = $last - $first + 1; - + if ( $first > $size ) { # invalid (past end of file) $response->code(416); @@ -1553,7 +1594,7 @@ sub sendStreamingFile { closeHTTPSocket($httpClient); return; } - + if ( $total < 1 ) { # invalid (first > last) $response->code(400); @@ -1562,15 +1603,15 @@ sub sendStreamingFile { closeHTTPSocket($httpClient); return; } - + if ( $last >= $size ) { $last = $size - 1; } - + main::DEBUGLOG && $log->is_debug && $log->debug("Handling Range request: $first-$last"); - + seek $fh, $first, 0; - + if ( $isTSR ) { # DLNA 7.4.40.7 A time seek uses 200 status and doesn't include Content-Range, ugh $response->code(200); } @@ -1579,31 +1620,31 @@ sub sendStreamingFile { $response->header( 'Content-Range' => "bytes $first-$last/$size" ); } $response->content_length( $total ); - + # Save total value for use later in sendStreamingResponse ${*$fh}{rangeTotal} = $total; ${*$fh}{rangeCounter} = 0; } } - + # Respond to realTimeInfo.dlna.org (DLNA 7.4.72) if ( $response->request->header('realTimeInfo.dlna.org') ) { $response->header( 'realTimeInfo.dlna.org' => 'DLNA.ORG_TLAG=*' ); } my $headers = _stringifyHeaders($response) . $CRLF; - + # For a range request, reduce rangeCounter to account for header size if ( ${*$fh}{rangeTotal} ) { ${*$fh}{rangeCounter} -= length $headers; } - + $streamingFiles{$httpClient} = $fh; # we are not a real streaming session, so we need to avoid sendStreamingResponse using the random $client stored in # $peerclient as this will cause streaming to the real client $client to stop. delete $peerclient{$httpClient}; - + # Disable metadata in case this client sent an Icy-Metadata header $sendMetaData{$httpClient} = 0; @@ -1727,7 +1768,7 @@ sub contentHasBeenModified { } } } - + if ($response->code() eq RC_NOT_MODIFIED) { for my $header (qw(Content-Length Content-Type Last-Modified)) { @@ -1744,7 +1785,7 @@ sub prepareResponseForSending { my ($client, $params, $body, $httpClient, $response) = @_; use bytes; - + # Trap empty content $body ||= \''; @@ -1846,13 +1887,13 @@ sub addHTTPResponse { # try to write out multibyte characters with invalid byte lengths in # sendResponse() below. use bytes; - + # Collect all our output into one chunk, to reduce TCP packets my $outbuf; # First add the headers, if requested if (!defined($sendheaders) || $sendheaders == 1) { - + # Add a header displaying the time it took us to serve this request $response->header( 'X-Time-To-Serve' => ( Time::HiRes::time() - $httpClient->start_time ) ); @@ -1864,16 +1905,16 @@ sub addHTTPResponse { if ($response->request()->method() ne 'HEAD' && $response->code() ne RC_NOT_MODIFIED && $response->code() ne RC_PRECONDITION_FAILED) { - + # use chunks if we have a transfer-encoding that says so if ($chunked) { - + # add chunk... $outbuf .= sprintf("%X", length($$body)) . $CRLF . $$body . $CRLF; - + # add a last empty chunk if we're closing the connection or if there's nothing more if ($close || !$more) { - + $outbuf .= '0' . $CRLF . $CRLF; } @@ -1882,7 +1923,7 @@ sub addHTTPResponse { $outbuf .= $$body; } } - + push @{$outbuf{$httpClient}}, { 'data' => \$outbuf, 'offset' => 0, @@ -1896,7 +1937,7 @@ sub addHTTPResponse { sub addHTTPLastChunk { my $httpClient = shift; my $close = shift; - + my $emptychunk = "0" . $CRLF . $CRLF; push @{$outbuf{$httpClient}}, { @@ -1905,7 +1946,7 @@ sub addHTTPLastChunk { 'length' => length($emptychunk), 'close' => $close, }; - + Slim::Networking::Select::addWrite($httpClient, \&sendResponse); } @@ -1970,9 +2011,9 @@ sub sendResponse { $segment->{'length'} -= $sentbytes; $segment->{'offset'} += $sentbytes; unshift @{$outbuf{$httpClient}}, $segment; - + } else { - + main::INFOLOG && $log->is_info && $log->info("Sent $sentbytes to $peeraddr{$httpClient}:$port"); # sent full message @@ -1981,10 +2022,10 @@ sub sendResponse { # no more messages to send main::INFOLOG && $log->is_info && $log->info("No more segments to send to $peeraddr{$httpClient}:$port"); - + # close the connection if requested by the higher God pushing segments if ($segment->{'close'} && $segment->{'close'} == 1) { - + main::INFOLOG && $log->is_info && $log->info("End request, connection closing for: $peeraddr{$httpClient}:$port"); closeHTTPSocket($httpClient); @@ -2009,7 +2050,7 @@ sub sendResponse { main::INFOLOG && $log->is_info && $log->info("More segments to send to $peeraddr{$httpClient}:$port"); } - + # Reset keep-alive timer Slim::Utils::Timers::killTimers( $httpClient, \&closeHTTPSocket ); Slim::Utils::Timers::setTimer( @@ -2042,14 +2083,14 @@ sub addStreamingResponse { # Set the kernel's send buffer to be higher so that there is less # chance of audio skipping if/when we block elsewhere in the code. - # + # # Check to make sure that our target size isn't smaller than the # kernel's default size. if (unpack('I', getsockopt($httpClient, SOL_SOCKET, SO_SNDBUF)) < (MAXCHUNKSIZE * 2)) { setsockopt($httpClient, SOL_SOCKET, SO_SNDBUF, (MAXCHUNKSIZE * 2)); } - + # we aren't going to read from this socket anymore so don't select on it... Slim::Networking::Select::removeRead($httpClient); @@ -2058,10 +2099,10 @@ sub addStreamingResponse { $client->streamingsocket($httpClient); my $newpeeraddr = getpeername($httpClient); - + $client->paddr($newpeeraddr) if $newpeeraddr; } - + Slim::Networking::Select::addWrite($httpClient, \&sendStreamingResponse, 1); } @@ -2076,31 +2117,31 @@ sub sendStreamingResponse { my $sentbytes; my $client; - + my $isInfo = ( main::INFOLOG && $log->is_info ) ? 1 : 0; - + if ( $peerclient{$httpClient} ) { $client = Slim::Player::Client::getClient($peerclient{$httpClient}); } - + # when we are streaming a file, we may not have a client, rather it might just be going to a web browser. # assert($client); - + my $outbuf = $outbuf{$httpClient}; my $segment = shift(@$outbuf); my $streamingFile = $streamingFiles{$httpClient}; my $silence = 0; - + main::INFOLOG && $isInfo && $log->info("sendStreaming response begun..."); - + # Keep track of where we need to stop if this is a range request my $rangeTotal; my $rangeCounter; if ( $streamingFile && ${*$streamingFile}{rangeTotal} ) { $rangeTotal = ${*$streamingFile}{rangeTotal}; $rangeCounter = ${*$streamingFile}{rangeCounter}; - + main::DEBUGLOG && $log->is_debug && $log->debug( " range request, sending $rangeTotal bytes ($rangeCounter sent)" ); } @@ -2125,7 +2166,7 @@ sub sendStreamingResponse { $silence = 1; } - + # if we don't have anything in our queue, then get something if (!defined($segment)) { @@ -2161,7 +2202,7 @@ sub sendStreamingResponse { my $chunk = undef; my $len = MAXCHUNKSIZE; - + # Reduce len if needed for a range request if ( $rangeTotal && ( $rangeCounter + $len > $rangeTotal ) ) { $len = $rangeTotal - $rangeCounter; @@ -2197,9 +2238,9 @@ sub sendStreamingResponse { # otherwise, queue up the next chunk of sound if ($chunkRef) { - + if (length($$chunkRef)) { - + if ( main::INFOLOG && $isInfo ) { $log->info("(audio: " . length($$chunkRef) . " bytes)"); } @@ -2209,9 +2250,9 @@ sub sendStreamingResponse { 'offset' => 0, 'length' => length($$chunkRef) ); - + unshift @$outbuf,\%segment; - + } else { main::INFOLOG && $log->info("Found an empty chunk on the queue - dropping the streaming connection."); forgetClient($client); @@ -2224,9 +2265,9 @@ sub sendStreamingResponse { my $retry = RETRY_TIME; main::INFOLOG && $isInfo && $log->info("Nothing to stream, let's wait for $retry seconds..."); - + Slim::Networking::Select::removeWrite($httpClient); - + Slim::Utils::Timers::setTimer($client, Time::HiRes::time() + $retry, \&tryStreamingLater,($httpClient)); } } @@ -2234,7 +2275,7 @@ sub sendStreamingResponse { # try again... $segment = shift(@$outbuf); } - + # try to send metadata, if appropriate if ($sendMetaData{$httpClient}) { @@ -2265,7 +2306,7 @@ sub sendStreamingResponse { ); $segment = \%segment; - + $metaDataBytes{$httpClient} = 0; if ( main::INFOLOG && $isInfo ) { @@ -2275,22 +2316,22 @@ sub sendStreamingResponse { } elsif (defined($segment) && $metaDataBytes{$httpClient} + $segment->{'length'} > METADATAINTERVAL) { my $splitpoint = METADATAINTERVAL - $metaDataBytes{$httpClient}; - + # make a copy of the segment, and point to the second half, to be sent later. my %splitsegment = %$segment; $splitsegment{'offset'} += $splitpoint; $splitsegment{'length'} -= $splitpoint; - + unshift @$outbuf, \%splitsegment; - + #only send the first part $segment->{'length'} = $splitpoint; - + $metaDataBytes{$httpClient} += $splitpoint; main::INFOLOG && $isInfo && $log->info("splitting message for metadata at $splitpoint"); - + } elsif (defined $segment) { # if it's time to send the metadata, just send the metadata @@ -2350,7 +2391,7 @@ sub sendStreamingResponse { if ($sentbytes) { main::INFOLOG && $isInfo && $log->info("Streamed $sentbytes to $peeraddr{$httpClient}"); - + # Update sent counter if this is a range request if ( $rangeTotal ) { ${*$streamingFile}{rangeCounter} += $sentbytes; @@ -2366,7 +2407,7 @@ sub tryStreamingLater { if ( defined $client->streamingsocket() && $httpClient == $client->streamingsocket() ) { - # Bug 10085 - This might be a callback for an old connection + # Bug 10085 - This might be a callback for an old connection # which we decided to close after establishing the timer, so # only kill the timer if we were called for the active streaming connection; # otherwise we might kill the timer related to the next connection too. @@ -2387,11 +2428,11 @@ sub forgetClient { sub closeHTTPSocket { my ( $httpClient, $streaming, $reason ) = @_; - + $reason ||= 'closed normally'; - + main::INFOLOG && $log->is_info && $log->info("Closing HTTP socket $httpClient with $peeraddr{$httpClient}:" . ($httpClient->peerport || 0). " ($reason)"); - + Slim::Utils::Timers::killTimers( $httpClient, \&closeHTTPSocket ); Slim::Networking::Select::removeRead($httpClient); @@ -2405,17 +2446,17 @@ sub closeHTTPSocket { delete($peeraddr{$httpClient}); delete($keepAlives{$httpClient}); delete($peerclient{$httpClient}); - + # heads up to handlers, if any for my $func (@closeHandlers) { if (ref($func) eq 'CODE') { - + # XXX: should this use eval? &{$func}($httpClient); } } - - + + # Fix for bug 1289. A close on its own wasn't always actually # sending a FIN or RST packet until significantly later for # streaming connections. The call to shutdown seems to be a @@ -2433,7 +2474,7 @@ sub closeHTTPSocket { sub closeStreamingSocket { my $httpClient = shift; - + if (defined $streamingFiles{$httpClient}) { main::INFOLOG && $log->is_info && $log->info("Closing streaming file."); @@ -2448,7 +2489,7 @@ sub closeStreamingSocket { $client->streamingsocket(undef); } } - + # Close socket unless it's keep-alive if ( $keepAlives{$httpClient} ) { main::INFOLOG && $log->is_info && $log->info('Keep-alive on streaming socket'); @@ -2496,7 +2537,7 @@ sub checkAuthorization { $ok = (crypt($password, $salt) eq $pwd); } } - + # Check for scanner progress request if ( !$ok && $pwd eq $password ) { if ( $request->header('X-Scanner') ) { @@ -2518,20 +2559,61 @@ sub checkAuthorization { return $ok; } +sub checkCORS { + my ($request, $response) = @_; + + if ( my $allowedHosts = $prefs->get('corsAllowedHosts') ) { + my ($host, $origin); + + eval { + $host = $request->header('Host'); + $origin = $request->header('Origin') || $request->header('Referer'); + }; + + if ($origin && $host) { + my ($h, $p) = Slim::Utils::Misc::crackURL($origin); + + # if the Host request header lists no port, crackURL() reports it as port 80, so we should + # pretend the Host header specified port 80 if it did not + $host .= ':80' if $host !~ m/:\d{1,}$/; + my $originHost = "$h:$p"; + + if ("$h:$p" ne $host) { + my $uri = URI->new($origin)->as_string; + $uri =~ s/(:\/\/.*?)\//$1/; + + my ($match) = grep { + $_ =~ s/(:\/\/.*?)\//$1/; + $_ eq $uri; + } split /[,\s]+/, $allowedHosts; + + if ($match) { + $response->header('Access-Control-Allow-Origin' => $origin); + } + elsif (main::INFOLOG && $log->is_info) { + $log->info("CORS is not enabled for $origin"); + } + } + } + } + + return $response->header('Access-Control-Allow-Origin'); +} + # addCloseHandler # defines a function to be called when $httpClient is closed # prototype: func($httpClient), no return value sub addCloseHandler{ my $funcPtr = shift; - + if ( main::DEBUGLOG && $log->is_debug ) { my $funcName = Slim::Utils::PerlRunTime::realNameForCodeRef($funcPtr); $log->debug("Adding Close handler: $funcName"); } - + push @closeHandlers, $funcPtr; } - + # Fills the template file specified as $path, using either the currently # selected skin, or an override. Returns the filled template string @@ -2606,7 +2688,7 @@ sub protect { if ( main::WEBUI ) { sub downloadMusicFile { my ($httpClient, $response, $id) = @_; - + # Support transferMode.dlna.org (DLNA 7.4.49) my $tm = $response->request->header('transferMode.dlna.org') || 'Streaming'; if ( $tm =~ /^(?:Streaming|Background)$/i ) { @@ -2623,24 +2705,27 @@ sub downloadMusicFile { my $obj = Slim::Schema->find('Track', $id); if (blessed($obj) && Slim::Music::Info::isSong($obj) && Slim::Music::Info::isFile($obj->url)) { - + # Bug 8808, support transcoding if a file extension is provided my $uri = $response->request->uri; my $isHead = $response->request->method eq 'HEAD'; if ( my ($outFormat) = $uri =~ m{download\.([^\?]+)} ) { - $outFormat = 'flc' if $outFormat eq 'flac'; - + # if the file extension itself is no valid content type, try to figure it out + if ( !Slim::Music::Info::isSong(undef, $outFormat) ) { + $outFormat = Slim::Music::Info::typeFromSuffix($uri); + } + if ( $obj->content_type ne $outFormat ) { if ( main::TRANSCODING ) { # Also support LAME bitrate/quality my ($bitrate) = $uri =~ m{bitrate=(\d+)}; my ($quality) = $uri =~ m{quality=(\d)}; $quality = 9 unless $quality =~ /^[0-9]$/; - + # Use aif because DLNA specifies big-endian format $outFormat = 'aif' if $outFormat =~ /^(?:aiff?|wav)$/; - + my ($transcoder, $error) = Slim::Player::TranscodingHelper::getConvertCommand2( $obj, undef, # content-type will be determined from $obj @@ -2650,58 +2735,51 @@ sub downloadMusicFile { $outFormat, $bitrate || 0, ); - + if ( !$transcoder ) { $log->error("Couldn't transcode " . $obj->url . " to $outFormat: $error"); - + $response->code(400); $response->headers->remove_content_headers; addHTTPResponse($httpClient, $response, \'', 1, 0); return 1; } - + my $command = Slim::Player::TranscodingHelper::tokenizeConvertCommand2( $transcoder, $obj->path, $obj->url, undef, $quality ); - + if ( !$command ) { $log->error("Couldn't create transcoder command-line for " . $obj->url . " to $outFormat"); - + $response->code(400); $response->headers->remove_content_headers; addHTTPResponse($httpClient, $response, \'', 1, 0); return 1; } - + my $in; my $out; my $done = 0; - + if ( !$isHead ) { main::INFOLOG && $log->is_info && $log->info("Opening transcoded download (" . $transcoder->{profile} . "), command: $command"); - + # Bug: 4318 # On windows ensure a child window is not opened if $command includes transcode processes if (main::ISWINDOWS) { Win32::SetChildShowWindow(0); $in = FileHandle->new; my $pid = $in->open($command); - - # XXX Bug 15650, this sets the priority of the cmd.exe process but not the actual - # transcoder process(es). - my $handle; - if ( Win32::Process::Open( $handle, $pid, 0 ) ) { - $handle->SetPriorityClass( Slim::Utils::OS::Win32::getPriorityClass() || Win32::Process::NORMAL_PRIORITY_CLASS() ); - } - + Win32::SetChildShowWindow(); } else { $in = FileHandle->new($command); } - + Slim::Utils::Network::blocking($in, 0); } - + if ($outFormat eq 'aif') { # Construct special PCM content-type $response->content_type( 'audio/L16;rate=' . $obj->samplerate . ';channels=' . $obj->channels ); @@ -2709,23 +2787,23 @@ sub downloadMusicFile { else { $response->content_type( $Slim::Music::Info::types{$outFormat} ); } - + # Tell client range requests are not supported $response->header( 'Accept-Ranges' => 'none' ); - + my $filename = Slim::Utils::Misc::pathFromFileURL($obj->url); $filename =~ s/\..+$/\.$outFormat/; $response->header('Content-Disposition', sprintf('attachment; filename="%s"', basename($filename)) ); - + my $is11 = $response->request->protocol eq 'HTTP/1.1'; - + if ($is11) { # Use chunked TE for HTTP/1.1 clients $response->header( 'Transfer-Encoding' => 'chunked' ); } - + # Add DLNA HTTP header, with ORG_CI to indicate transcoding, and lack of ORG_OP to indicate no seeking my $dlna; if ( $outFormat eq 'mp3' ) { @@ -2737,7 +2815,7 @@ sub downloadMusicFile { if ($dlna) { $response->header( 'contentFeatures.dlna.org' => $dlna ); } - + my $headers = _stringifyHeaders($response) . $CRLF; # non-blocking stream $pipeline to $httpClient @@ -2745,22 +2823,22 @@ sub downloadMusicFile { if ($headers) { syswrite $httpClient, $headers; undef $headers; - + if ($isHead) { $done = 1; } } - + if ($done) { $out && $out->destroy; $in && $in->close; - + if ( $httpClient->opened() ) { closeHTTPSocket($httpClient); } return; } - + if ($in) { # Try to read some data from the pipeline my $len = sysread $in, my $buf, 32 * 1024; @@ -2772,7 +2850,7 @@ sub downloadMusicFile { } elsif ( $len == 0 ) { $done = 1; - + if ($is11) { # Add last empty chunk $out->push_write( '0' . $CRLF . $CRLF ); @@ -2791,7 +2869,7 @@ sub downloadMusicFile { } } }; - + $out = AnyEvent::Handle->new( fh => $httpClient, linger => 0, @@ -2806,13 +2884,13 @@ sub downloadMusicFile { main::INFOLOG && $log->is_info && $log->info("Transcoded download error: $msg"); $done = 1; $writer->(); - }, + }, ); - + # Bug 17212, Must add callback after object creation - references to $out within the $writer callback were # failing when the on_drain callback was passed as a constructor argument $out->on_drain($writer); - + return 1; } else { @@ -2826,23 +2904,23 @@ sub downloadMusicFile { } } } - + main::INFOLOG && $log->is_info && $log->info("Opening $obj for download..."); - + my $ct = $Slim::Music::Info::types{$obj->content_type()}; - + # Add DLNA HTTP header if ( my $pn = $obj->dlna_profile ) { my $canseek = ($pn eq 'MP3' || $pn =~ /^WMA/); my $dlna = "DLNA.ORG_PN=${pn};DLNA.ORG_OP=" . ($canseek ? '11' : '01') . ";DLNA.ORG_FLAGS=01700000000000000000000000000000"; $response->header( 'contentFeatures.dlna.org' => $dlna ); } - + Slim::Web::HTTP::sendStreamingFile( $httpClient, $response, $ct, Slim::Utils::Misc::pathFromFileURL($obj->url), $obj ); - + return 1; } - + return; } @@ -2858,7 +2936,7 @@ sub downloadVideoFile { my $dlna = "DLNA.ORG_PN=${pn};DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000"; $response->header( 'contentFeatures.dlna.org' => $dlna ); } - + # Support transferMode.dlna.org (DLNA 7.4.49) my $tm = $response->request->header('transferMode.dlna.org') || 'Streaming'; if ( $tm =~ /^(?:Streaming|Background)$/i ) { @@ -2871,11 +2949,11 @@ sub downloadVideoFile { closeHTTPSocket($httpClient); return; } - + Slim::Web::HTTP::sendStreamingFile( $httpClient, $response, $video->{mime_type}, Slim::Utils::Misc::pathFromFileURL($video->{url}), $video ); return 1; } - + return; } @@ -2891,7 +2969,7 @@ sub downloadImageFile { my $dlna = "DLNA.ORG_PN=${pn};DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000"; $response->header( 'contentFeatures.dlna.org' => $dlna ); } - + # Support transferMode.dlna.org (DLNA 7.4.49) my $tm = $response->request->header('transferMode.dlna.org') || 'Interactive'; if ( $tm =~ /^(?:Interactive|Background)$/i ) { @@ -2904,11 +2982,11 @@ sub downloadImageFile { closeHTTPSocket($httpClient); return; } - + Slim::Web::HTTP::sendStreamingFile( $httpClient, $response, $image->{mime_type}, Slim::Utils::Misc::pathFromFileURL($image->{url}), $image ); return 1; } - + return; } diff --git a/Slim/Web/HTTP/CSRF.pm b/Slim/Web/HTTP/CSRF.pm index 55797b6cad8..50da158d348 100644 --- a/Slim/Web/HTTP/CSRF.pm +++ b/Slim/Web/HTTP/CSRF.pm @@ -1,8 +1,7 @@ package Slim::Web::HTTP::CSRF; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/HTTP/ClientConn.pm b/Slim/Web/HTTP/ClientConn.pm index e2f1ac10728..0a9e333307c 100644 --- a/Slim/Web/HTTP/ClientConn.pm +++ b/Slim/Web/HTTP/ClientConn.pm @@ -1,6 +1,5 @@ package Slim::Web::HTTP::ClientConn; -# $Id$ # Subclass of HTTP::Daemon::ClientConn that represents a web client diff --git a/Slim/Web/ImageProxy.pm b/Slim/Web/ImageProxy.pm index c842ea28845..519e72706ca 100644 --- a/Slim/Web/ImageProxy.pm +++ b/Slim/Web/ImageProxy.pm @@ -1,8 +1,8 @@ package Slim::Web::ImageProxy; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. =head1 NAME @@ -12,10 +12,10 @@ Slim::Web::ImageProxy =head1 SYNOPSIS use Slim::Web::ImageProxy; - + # get an artwork file's url, including potential proxy path my $resize_url = Slim::Web::ImageProxy::proxiedImage($songData->{artwork_url}); - + # register a custom handler to define an image's URL based on some URL Slim::Web::ImageProxy->registerHandler( match => qr/someradio\.com\/graphics\/covers\/[sml]\/.*/, @@ -29,24 +29,24 @@ Slim::Web::ImageProxy 300 => 'l', }) || 'l'; $url =~ s/\/[sml]\//\/$size\//; - + return $url; }, ); - + # register an external image proxy to be used for all external images # using an external resizing proxy can improve performance on slow LMS systems - Slim::Web::ImageProxy->registerHandler( - id => 'someExternalImageProxy', + Slim::Web::ImageProxy->registerHandler( + id => 'someExternalImageProxy', func => sub { my ($url, $spec) = @_; - + # parse the resizing parameters my ($width, $height, $mode, $bgcolor, $ext) = Slim::Web::Graphics->parseSpec($spec); - + # return the full URL to your resizing service return sprintf( - 'http://www.yourdomain.com/imageresizer?url=%s&width=%s&height=%s', + 'http://www.yourdomain.com/imageresizer?url=%s&width=%s&height=%s', uri_escape_utf8($url), $width, $height @@ -54,14 +54,14 @@ Slim::Web::ImageProxy }, external => 1, desc => 'Some fast external image proxy' - ); - + ); + =head1 DESCRIPTION The ImageProxy module allows you to have artwork resized on the server-side without relying on mysqueezebox.com -Besides resizing artwork, custom handlers can eg. convert track IDs to image URLs, build a query string to request +Besides resizing artwork, custom handlers can eg. convert track IDs to image URLs, build a query string to request the smallest image possible for the given resizing spec etc. By registering an external image proxy you can off-load the downloading and resizing of potentially large external @@ -98,26 +98,26 @@ my $cache; my %queue; sub init { - $cache ||= Slim::Web::ImageProxy::Cache->new(); + $cache ||= Slim::Web::ImageProxy::Cache->new(); # clean up stale cache files - Slim::Utils::Misc::deleteFiles($prefs->get('cachedir'), qr/^imgproxy_[a-f0-9]+$/i); + Slim::Utils::Misc::deleteFiles($prefs->get('cachedir'), qr/^imgproxy_[a-f0-9]+$/i); } sub getImage { my ($class, $client, $path, $params, $callback, $spec, @args) = @_; - + main::DEBUGLOG && $log->debug("Get artwork for URL: $path"); # check the cache for this url - $cache ||= Slim::Web::ImageProxy::Cache->new(); - - my $path2; + $cache ||= Slim::Web::ImageProxy::Cache->new(); + + my $path2; - # some clients require the .png ending, but we don't store it in the cache - try them both - if ($path =~ /(.*)\.png$/) { + # some clients require the .png ending, but we don't store it in the cache - try them both + if ($path =~ /(.*)\.png$/) { $path2 = $1; - } + } if ( my $cached = $cache->get($path) || ($path2 && $cache->get($path2)) ) { main::DEBUGLOG && $log->is_debug && $log->debug( 'Got cached artwork of type ' . $cached->{content_type} . ' and ' . length(${$cached->{data_ref}}) . ' bytes length' ); @@ -125,10 +125,10 @@ sub getImage { _setHeaders($args[1], $cached->{content_type}); $callback && $callback->( $client, $params, $cached->{data_ref}, @args ); - + return; } - + my ($url) = $path =~ m|imageproxy/(.*)/[^/]*|; if ( !$url ) { @@ -140,10 +140,10 @@ sub getImage { my $handleProxiedUrl = sub { my $url = shift; - + if ( !$url || $url !~ /^(?:file|https?):/i ) { main::INFOLOG && $log->info("No artwork found, returning 404"); - + _artworkError( $client, $params, $spec, 404, $callback, @args ); return; } @@ -155,20 +155,20 @@ sub getImage { if ( $url =~ /^https?:/ && $spec && $spec !~ /^\.(png|jpe?g)/i && (my $imageproxy = $prefs->get('useLocalImageproxy')) ) { if ( my $external = $externalHandlers{$imageproxy} ) { my ($host, $port, $path, $user, $pass) = Slim::Utils::Misc::crackURL($url); - + if ( $external->{func} && !($host && (Slim::Utils::Network::ip_is_private($host) || $host =~ /localhost/i)) ) { my $url2 = $external->{func}->(uri_escape_utf8($url), $spec); $url = $url2 if $url2; $pre_shrunk = 1; - + main::DEBUGLOG && $log->debug("Using custom image proxy: $url"); } } } - + $queue{$url} ||= []; - - # we're going to queue up requests, so we don't need to download + + # we're going to queue up requests, so we don't need to download # the same file multiple times due to a race condition push @{ $queue{$url} }, { client => $client, @@ -179,7 +179,7 @@ sub getImage { args => \@args, pre_shrunk => $pre_shrunk, }; - + if ( $url =~ /^file:/ ) { my $path = Slim::Utils::Misc::pathFromFileURL($url); _resizeFromFile($url, $path); @@ -187,7 +187,7 @@ sub getImage { elsif ( $url =~ /^https?:/ ) { # no need to do the http request if we're already fetching it return if scalar @{ $queue{$url} } > 1; - + my $http = Slim::Networking::SimpleAsyncHTTP->new( \&_gotArtwork, \&_gotArtworkError, @@ -196,56 +196,51 @@ sub getImage { cache => 1, }, ); - + $http->get( $url ); } }; - + # some plugin might have registered to deal with this image URL if ( my $handler = $class->getHandlerFor($url) ) { $url = $handler->($url, $spec, $handleProxiedUrl); return unless defined $url; } - + $handleProxiedUrl->($url); } sub _gotArtwork { my $http = shift; my $url = $http->url; - + if (main::DEBUGLOG && $log->is_debug) { $log->debug('Received artwork of type ' . $http->headers->content_type . ' and ' . ($http->headers->content_length || length(${$http->contentRef})) . ' bytes length' ); } - + # shortcut if we received an error message back if ($http->headers->content_type =~ /text/) { - return _gotArtworkError($http); - } + # Content-Type might be incorrectly set - try to guess from magic bytes + require Slim::Utils::GDResizer; + my $filetype = Slim::Utils::GDResizer::_content_type($http->contentRef, 1); - if ( Slim::Utils::ImageResizer::hasDaemon() ) { - # We don't use SimpleAsyncHTTP's saveAs feature, as this wouldn't keep a copy in the cache, which we might need - # if we wanted other sizes of the same url - my $fullpath = catdir( $prefs->get('cachedir'), 'imgproxy_' . Digest::MD5::md5_hex($url) ); - - # Unfortunately we have to write the data to a file, in case LMS was using an external image resizer (TinyLMS) - File::Slurp::write_file($fullpath, $http->contentRef); - - _resizeFromFile($http->url, $fullpath); - - unlink $fullpath; - } - else { - _resizeFromFile($http->url, $http->contentRef, $http); + if ($filetype) { + main::INFOLOG && $log->is_info && $log->info("File type actually seems to be '$filetype', rather than " . $http->headers->content_type); + } + else { + return _gotArtworkError($http); + } } + + _resizeFromFile($http->url, $http->contentRef, $http); } sub _gotArtworkError { my $http = shift; my $url = $http->url; - + my $error = 404; - + if ($http->headers && $http->headers->content_type =~ /text/) { $error = 500; main::INFOLOG && $log->is_info && $log->info("Server returned error: " . $http->content); @@ -254,6 +249,9 @@ sub _gotArtworkError { $error = $1; main::INFOLOG && $log->is_info && $log->info("Server returned error: " . $http->error); } + elsif (main::INFOLOG && $log->is_info) { + $log->info("Unexpected failure fetching $url: " . $http->error); + } # File does not exist, return error main::INFOLOG && $log->info("Artwork not found, returning $error: " . $url); @@ -264,17 +262,17 @@ sub _gotArtworkError { my $args = $item->{args}; my $params = $item->{params}; my $callback = $item->{callback}; - + _artworkError( $client, $params, $spec, $error, $callback, @$args ); } - + delete $queue{$url}; } sub _resizeFromFile { my ($url, $fullpath, $http) = @_; - $cache ||= Slim::Web::ImageProxy::Cache->new(); + $cache ||= Slim::Web::ImageProxy::Cache->new(); while ( my $item = shift @{ $queue{$url} }) { my $client = $item->{client}; @@ -283,7 +281,7 @@ sub _resizeFromFile { my $params = $item->{params}; my $callback = $item->{callback}; my $cachekey = $item->{cachekey}; - + # no need to resize data if we've got it from an external image proxy if ( ($spec =~ /^\.(?:png|jpe?g)/i || $item->{pre_shrunk}) && $http && $http->headers->content_type =~ /image\/(png|jpe?g)/ ) { main::DEBUGLOG && $log->debug("No resizing required - already resized remotely, or original size requested"); @@ -297,7 +295,7 @@ sub _resizeFromFile { original_path => undef, data_ref => $fullpath, } ); - + _setHeaders($args->[1], $ct); $callback && $callback->( $client, $params, $fullpath, @$args ); @@ -305,10 +303,12 @@ sub _resizeFromFile { else { Slim::Utils::ImageResizer->resize($fullpath, $cachekey, $spec, sub { my ($body, $format) = @_; - + # Resized image should now be in cache my $response = $args->[1]; - + + unlink $fullpath if !ref $fullpath && $fullpath =~ /imgproxy_[a-f0-9]+$/i; + if ( !($body && $format && ref $body eq 'SCALAR') && (my $c = $cache->get($cachekey)) ) { $body = $c->{data_ref}; $format = $c->{content_type}; @@ -317,20 +317,20 @@ sub _resizeFromFile { # resize command failed, return 500 main::INFOLOG && $log->info(" Resize failed, returning 500"); $log->error("Artwork resize for $cachekey failed"); - + _artworkError( $client, $params, $spec, 500, $callback, @$args ); return; } - + if ($body && $format) { _setHeaders($args->[1], $format); } - + $callback && $callback->( $client, $params, $body, @$args ); }, $cache ); } } - + delete $queue{$url}; } @@ -351,7 +351,7 @@ sub _artworkError { my $response = $args[1]; my ($width, $height, $mode, $bgcolor, $ext) = Slim::Web::Graphics->parseSpec($spec); - + require Slim::Utils::GDResizer; my ($res, $format) = Slim::Utils::GDResizer->resize( file => Slim::Web::HTTP::fixHttpPath($params->{'skinOverride'} || $prefs->get('skin'), 'html/images/radio.png'), @@ -367,7 +367,7 @@ sub _artworkError { # $response->code($code); $response->expires( time() - 1 ); $response->header( 'Cache-Control' => 'no-cache' ); - + $callback->( $client, $params, $res, @args ); } @@ -378,7 +378,7 @@ sub _artworkError { # - or the $force parameter is passed in # # $force can be used to create custom handlers dealing with custom "urls". -# Eg. there could be a pattern to only pass some album id, together with a keyword, +# Eg. there could be a pattern to only pass some album id, together with a keyword, # like "spotify::album::123456". It's then up to the image handler to get the real url. sub proxiedImage { my ($url, $force) = @_; @@ -390,44 +390,46 @@ sub proxiedImage { return $url unless main::NOMYSB || $force || $prefs->get('useLocalImageproxy') || __PACKAGE__->getHandlerFor($url); # main::DEBUGLOG && $log->debug("Use proxied image URL for: $url"); - + + # Unfortunately Squeezeplay can't handle images without a file extension?!? We need to return some extension, + # though it might be different from the actual content type returned. But we don't know better at this point. my $ext = '.png'; - + if ($url =~ /(\.(?:jpg|jpeg|png|gif))/) { $ext = $1; $ext =~ s/jpeg/jpg/; } - + return '/imageproxy/' . uri_escape_utf8($url) . '/image' . $ext; } # allow plugins to register custom handlers for the image url sub registerHandler { my ( $class, %params ) = @_; - + if ( $params{external} && $params{desc} && $params{id} && $params{func} ) { $externalHandlers{ $params{id} } = \%params; main::DEBUGLOG && $log->is_debug && $log->debug("Registered external image proxy: " . $params{desc}); return 1; } - + if ( ref $params{match} ne 'Regexp' ) { $log->error( 'registerProvider called without a regular expression ' . ref $params{match} ); return; } - + if ( ref $params{func} ne 'CODE' ) { $log->error( 'registerProider called without a code reference' ); return; } - + $handlers{ $params{match} } = $params{func}; - + if ( main::DEBUGLOG && $log->is_debug ) { my $name = Slim::Utils::PerlRunTime::realNameForCodeRef( $params{func} ); $log->debug( "Registered new artwork URL handler for " . $params{match} . ": $name" ); } - + return 1; } @@ -442,7 +444,7 @@ sub getExternalHandlers { # helper method to get the correc sizing parameter out of a list of possible sizes # my $size = Slim::Web::ImageProxy->getRightSize("180x180_m.jpg", { -# 70 => 's', +# 70 => 's', # 150 => 'm', # 300 => 'l', # 600 => 'g', @@ -458,7 +460,7 @@ sub getRightSize { if ($width || $height) { $width ||= $height; $height ||= $width; - + my $min = ($width > $height ? $width : $height); # get smallest size larger than what we need @@ -492,16 +494,16 @@ sub new { Slim::Utils::Timers::setTimer( undef, time() + 10 + int(rand(5)), \&cleanup, 1 ); } } - + return $cache; } sub cleanup { my ($class, $force) = @_; - + # after startup don't purge if a player is on - retry later my $interval; - + unless ($force) { for my $client ( Slim::Player::Client::clients() ) { if ($client->power()) { @@ -511,14 +513,14 @@ sub cleanup { } } } - + my $now = time(); - + if (!$interval) { my $start = $now; - + $cache->purge; - + main::INFOLOG && $log->is_info && $log->info(sprintf("ImageProxy cache purge: %f sec", $now - $start)); } diff --git a/Slim/Web/JSONRPC.pm b/Slim/Web/JSONRPC.pm index 5416b5370ee..d82c98ed0a8 100644 --- a/Slim/Web/JSONRPC.pm +++ b/Slim/Web/JSONRPC.pm @@ -1,10 +1,9 @@ package Slim::Web::JSONRPC; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. # This class provides a JSON-RPC 1.0 API over HTTP to the Slim::Control::Request @@ -12,16 +11,17 @@ package Slim::Web::JSONRPC; use strict; -use HTTP::Status qw(RC_OK); +use HTTP::Status qw(RC_OK RC_FORBIDDEN); use JSON::XS::VersionOneAndTwo; use Scalar::Util qw(blessed); use Slim::Web::HTTP; use Slim::Utils::Compress; use Slim::Utils::Log; - +use Slim::Utils::Prefs; my $log = logger('network.jsonrpc'); +my $prefs = preferences('server'); # this holds a context for each connection, to enable asynchronous commands as well # as subscriptions. @@ -39,7 +39,7 @@ sub init { # register our URI handler Slim::Web::Pages->addRawFunction('jsonrpc.js', \&handleURI); - + # register our close handler # we want to be called if a connection closes to clear our context Slim::Web::HTTP::addCloseHandler(\&handleClose); @@ -53,10 +53,10 @@ sub handleClose { if (defined $contexts{$httpClient}) { main::DEBUGLOG && $log->debug("Closing any subscriptions for $httpClient"); - + # remove any subscription management Slim::Control::Request::unregisterAutoExecute($httpClient); - + # delete the context delete $contexts{$httpClient}; } @@ -72,30 +72,61 @@ sub handleURI { my ($httpClient, $httpResponse) = @_; main::DEBUGLOG && $log->debug("handleURI($httpClient)"); - + # make sure we're connected if (!$httpClient->connected()) { $log->warn("Aborting, client not connected: $httpClient"); return; } - + + # see Slim::Web::CSRF::isRequestCSRFSafe + if (!$httpResponse->header('Access-Control-Allow-Origin') && $prefs->get('csrfProtectionLevel') && (my $request = $httpResponse->request)) { + my ($host, $origin); + eval { + $host = $request->header('Host'); + $origin = $request->header('Origin') || $request->header('Referer'); + }; + + if ($origin && $host) { + my ($h, $p) = Slim::Utils::Misc::crackURL($origin); + + # if the Host request header lists no port, crackURL() reports it as port 80, so we should + # pretend the Host header specified port 80 if it did not + $host .= ':80' if $host !~ m/:\d{1,}$/ ; + + if ("$h:$p" ne $host) { + $log->warn("Not allowed from Origin: [$origin]"); + + $httpResponse->code(RC_FORBIDDEN); + $httpResponse->content_type('application/json'); + $httpResponse->header('Connection' => 'close'); + $httpResponse->content(''); + + $httpClient->send_response($httpResponse); + Slim::Web::HTTP::closeHTTPSocket($httpClient); + + return; + } + } + } + # cancel any previous subscription on this connection # we must have a context defined and a subscription defined if (defined($contexts{$httpClient}) && Slim::Control::Request::unregisterAutoExecute($httpClient)) { - + # we want to send a last chunk to close the connection as per HTTP... # a subscription is essentially a never ending response: we're receiving here # a new request (aka pipelining) so we want to be nice and close the previous response - + # we cannot have a subscription if this is not a long lasting, keep-open, chunked connection. - + Slim::Web::HTTP::addHTTPLastChunk($httpClient, 0); } - + # get the request data (POST for JSON 1.0) my $input = $httpResponse->request()->content(); - + if (!$input) { # No data @@ -112,22 +143,22 @@ sub handleURI { # Convert JSON to Perl # FIXME: JSON 1.0 accepts multiple requests ? How do we parse that efficiently? my $procedure = from_json($input); - - + + # Validate the procedure # We must get a JSON object, i.e. a hash if (ref($procedure) ne 'HASH') { - + $log->warn("Cannot parse POST data into Perl hash => closing connection"); - + Slim::Web::HTTP::closeHTTPSocket($httpClient); return; } - + if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "JSON parsed procedure: " . Data::Dump::dump($procedure) ); } - + # we must have a method my $method = $procedure->{'method'}; @@ -138,47 +169,58 @@ sub handleURI { Slim::Web::HTTP::closeHTTPSocket($httpClient); return; } - + # figure out the method wanted my $funcPtr = $methods{$method}; - + if (!$funcPtr) { # return error, not a known procedure $log->warn("Unknown method $method => closing connection"); - + Slim::Web::HTTP::closeHTTPSocket($httpClient); return; - + } elsif (ref($funcPtr) ne 'CODE') { # return internal server error $log->error("Procedure $method refers to non CODE ??? => closing connection"); - + Slim::Web::HTTP::closeHTTPSocket($httpClient); return; } - + # parse the parameters my $params = $procedure->{'params'}; if (ref($params) ne 'ARRAY') { - + # error, params is an array or an object $log->warn("Procedure $method has params not ARRAY => closing connection"); Slim::Web::HTTP::closeHTTPSocket($httpClient); return; } - - + + # block access to "pref" & "serverpref" commands if request is coming from external host + my $peeraddr = $Slim::Web::HTTP::peeraddr{$httpClient}; + if ( !Slim::Utils::Network::ip_is_host($peeraddr) + && $prefs->get('protectSettings') && !$prefs->get('authorize') + && $params->[1] && ref($params->[1]) && $params->[1]->[0] && $params->[1]->[0] =~ /^(?:pref|serverpref|stopserver|restartserver)/ + && ( Slim::Utils::Network::ip_is_gateway($peeraddr) || Slim::Utils::Network::ip_on_different_network($peeraddr) ) + ) { + $log->error("Access to settings is restricted to the local network or localhost: $peeraddr " . $httpResponse->request()->content()); + Slim::Web::HTTP::closeHTTPSocket($httpClient); + return; + } + # create a hash to store our context my $context = {}; $context->{'httpClient'} = $httpClient; $context->{'httpResponse'} = $httpResponse; $context->{'procedure'} = $procedure; - + # Detect the language the client wants content returned in if ( my $lang = $httpResponse->request->header('Accept-Language') ) { @@ -189,22 +231,22 @@ sub handleURI { if ( my $ua = ( $httpResponse->request->header('X-User-Agent') || $httpResponse->request->header('User-Agent') ) ) { $context->{ua} = $ua; } - + # Check our operational mode using our X-Jive header # We must be delaing with a 1.1 client because X-Jive uses chunked transfers # We must not be closing the connection if (defined(my $xjive = $httpResponse->request()->header('X-Jive')) && $httpClient->proto_ge('1.1') && $httpResponse->header('Connection') !~ /close/i) { - + main::INFOLOG && $log->info("Operating in x-jive mode for procedure $method and client $httpClient"); $context->{'x-jive'} = 1; $httpResponse->header('X-Jive' => 'Jive') } - + # remember we need to send headers. We'll reset this once sent. $context->{'sendheaders'} = 1; - + # store our context. It'll get erased by the callback in HTTP.pm through handleClose $contexts{$httpClient} = $context; @@ -228,16 +270,16 @@ sub handleURI { sub writeResponse { my $context = shift; my $responseRef = shift; - + my $isDebug = main::DEBUGLOG && $log->is_debug; - + my $httpClient = $context->{'httpClient'}; my $httpResponse = $context->{'httpResponse'}; if ( main::DEBUGLOG && $isDebug ) { $log->debug( "JSON response: " . Data::Dump::dump($responseRef) ); } - + # Don't waste CPU cycles if we're not connected if (!$httpClient->connected()) { $log->warn("Client disconnected in writeResponse!"); @@ -250,21 +292,21 @@ sub writeResponse { main::DEBUGLOG && $isDebug && $log->info("JSON raw response: [$jsonResponse]"); $httpResponse->code(RC_OK); - + # set a content type to 1.1 proposed value. Should work with 1.0 as it is not specified $httpResponse->content_type('application/json'); - + use bytes; - + # send the headers only once my $sendheaders = $context->{'sendheaders'}; if ($sendheaders) { $context->{'sendheaders'} = 0; } - + # in xjive mode, use chunked mode without a last chunk (i.e. we always have $more) my $xjive = $context->{'x-jive'}; - + if ($xjive) { $httpResponse->header('Transfer-Encoding' => 'chunked'); } else { @@ -280,12 +322,12 @@ sub writeResponse { } } } - + $httpResponse->content_length(length($jsonResponse)); } - + if ($sendheaders) { - + if ( main::DEBUGLOG && $isDebug ) { $log->debug("Response headers: [\n" . $httpResponse->as_string . "]"); } @@ -305,15 +347,15 @@ sub generateJSONResponse { # create an object for the response my $response = {}; - + # add ID if we have it if (defined(my $id = $context->{'procedure'}->{'id'})) { $response->{'id'} = $id; } - + # add result $response->{'result'} = $result; - + # while not strictly allowed, the JSON specs does not forbid to add the # request data to the response... $response->{'params'} = $context->{'procedure'}->{'params'}; @@ -334,12 +376,12 @@ sub requestMethod { if ( main::DEBUGLOG && $log->is_debug ) { $log->debug( "requestMethod(" . Data::Dump::dump($reqParams) . ")" ); } - + # current style : [, [cmd]] # proposed style: [{player:xxx, cmd:[xxx], params:{xxx}}] # benefit: more than one command in single request # HOW DOES RECEIVER PARSE??? - + my $commandargs = $reqParams->[1]; if (!$commandargs || ref($commandargs) ne 'ARRAY') { @@ -352,7 +394,7 @@ sub requestMethod { my $playername = scalar ($reqParams->[0]); my $client = Slim::Player::Client::getClient($playername); my $clientid = blessed($client) ? $client->id() : undef; - + if ($clientid) { # bug 16988 - need to update lastActivityTime in jsonrpc too $client->lastActivityTime( Time::HiRes::time() ); @@ -370,7 +412,7 @@ sub requestMethod { my $ua = $context->{ua}; my $finish; - + if ( $client ) { $finish = sub { $client->languageOverride(undef); @@ -390,14 +432,14 @@ sub requestMethod { if ( $ua && $client ) { $client->controllerUA($ua); } - + # fix the encoding and/or manage charset param $request->fixEncoding(); # remember we're the source and the $httpClient $request->source('JSONRPC'); $request->connectionID($context->{'httpClient'}); - + if ($context->{'x-jive'}) { # set this in case the query can be subscribed to $request->autoExecuteCallback(\&requestWrite); @@ -406,24 +448,24 @@ sub requestMethod { main::INFOLOG && $log->info("Dispatching..."); $request->execute(); - + if ($request->isStatusError()) { $finish->() if $finish; if ( $log->is_error ) { $log->error("Request failed with error: " . $request->getStatusText); } - + Slim::Web::HTTP::closeHTTPSocket($context->{'httpClient'}); return; } else { - + # handle async commands if ($request->isStatusProcessing()) { - + main::INFOLOG && $log->info("Request is async: will be back"); - + # add our write routine as a callback $request->callbackParameters( sub { requestWrite(@_); @@ -433,14 +475,14 @@ sub requestMethod { } $finish->() if $finish; - + # the request was successful and is not async, send results back to caller! requestWrite($request, $context->{'httpClient'}, $context); } - - } else { - $log->error("request not dispatchable!"); + } else { + $clientid ||= $playername; + $log->warn(($clientid ? "$clientid: " : '') . "request not dispatchable!"); Slim::Web::HTTP::closeHTTPSocket($context->{'httpClient'}); return; } @@ -456,18 +498,18 @@ sub requestWrite { my $context = shift; main::DEBUGLOG && $log->debug("requestWrite()"); - + if (!$httpClient) { - + # recover our http client $httpClient = $request->connectionID(); } - + if (!$context) { - + # recover our beloved context $context = $contexts{$httpClient}; - + if (!$context) { $log->error("Context not found in requestWrite!!!!"); return; diff --git a/Slim/Web/Pages.pm b/Slim/Web/Pages.pm index 22b3145caca..8142afa33b9 100644 --- a/Slim/Web/Pages.pm +++ b/Slim/Web/Pages.pm @@ -1,8 +1,7 @@ package Slim::Web::Pages; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Pages/BrowseDB.pm b/Slim/Web/Pages/BrowseDB.pm index 0a0605a2d6c..7255b1aa125 100644 --- a/Slim/Web/Pages/BrowseDB.pm +++ b/Slim/Web/Pages/BrowseDB.pm @@ -1,6 +1,6 @@ package Slim::Web::Pages::BrowseDB; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Pages/Common.pm b/Slim/Web/Pages/Common.pm index ad73a470eb1..19c7b6b25ae 100644 --- a/Slim/Web/Pages/Common.pm +++ b/Slim/Web/Pages/Common.pm @@ -1,8 +1,7 @@ package Slim::Web::Pages::Common; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -489,30 +488,43 @@ sub logFile { return; } - $response->header("Refresh" => "10; url=" . $params->{path} . ($params->{lines} ? '?lines=' . $params->{lines} : '')); - $response->header("Content-Type" => "text/plain; charset=utf-8"); - - my $count = ($params->{lines} * 1) || 50; + my $lines = $params->{lines}; + my $search = $params->{search}; + my $url = $params->{path}; + $url .= "?lines=$lines" if $lines; + $url .= ($lines ? '&' : '?') . "search=$search" if $search; + + # if we're called with a GET, then likely it was a link, use default refresh rate + $params->{refresh} = 10 if !defined $params->{refresh} && $response->request->method eq 'GET'; - my $body = ''; + my $count = ($lines * 1) || 50; + $params->{logLines} = ''; + my $file = File::ReadBackwards->new($logFile); if ($file){ my @lines; - while ( --$count && (my $line = $file->readline()) ) { + while ( $count && (my $line = $file->readline()) ) { + next if $search && $line !~ /\Q$search\E/i; + $line = "$line<\/span>" if $line =~ /main::init.*Starting/; - $line =~ s/(error)\b/$1<\/span>/ig; - $line =~ s/(warn.*?)\b/$1<\/span>/ig; + $line = qq($line<\/span>) if $line =~ /(error)\b/i; + $line = qq($line<\/span>) if $line =~ /(warn.*?)\b/i; unshift (@lines, $line); + + --$count; } - $body .= join('', @lines); + $params->{logLines} .= join('', @lines); $file->close(); - }; + }; + + $response->expires(0); + $response->header('Cache-Control' => 'no-cache'); - return ("text/html", \"
$body
"); + return Slim::Web::HTTP::filltemplatefile("log.html", $params); } sub statusM3u { diff --git a/Slim/Web/Pages/EditPlaylist.pm b/Slim/Web/Pages/EditPlaylist.pm index 70b03d475b2..ce29f34a9d8 100644 --- a/Slim/Web/Pages/EditPlaylist.pm +++ b/Slim/Web/Pages/EditPlaylist.pm @@ -1,8 +1,8 @@ package Slim::Web::Pages::EditPlaylist; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -96,30 +96,30 @@ sub saveCurrentPlaylist { $params->{'warning'} = 'FILENAME_WARNING'; } else { - + # Changed by Fred to fix the issue of getting the playlist object # by setting $p1 to it, which was messing up callback and the CLI. - + my $request = Slim::Control::Request::executeRequest($client, ['playlist', 'save', $title]); - + if (defined $request) { - + $params->{'playlist.id'} = $request->getResult('__playlist_id'); - + if ($request->getResult('writeError')) { - + $params->{'warning'} = $client->string('PLAYLIST_CANT_WRITE'); - + } } - + } # Don't add this back to the breadcrumbs delete $params->{'saveCurrentPlaylist'}; - + return browsePlaylist(@_); - + } else { return browsePlaylists(@_); @@ -132,39 +132,39 @@ sub renamePlaylist { my $newName = $params->{'newname'}; if ($newName ne Slim::Utils::Misc::cleanupFilename($newName)) { - + $params->{'warning'} = 'FILENAME_WARNING'; } else { - + my $playlist_id = $params->{'playlist_id'}; my $dry_run = !$params->{'overwrite'}; - - my $request = Slim::Control::Request::executeRequest(undef, [ - 'playlists', - 'rename', + + my $request = Slim::Control::Request::executeRequest($client, [ + 'playlists', + 'rename', 'playlist_id:' . $playlist_id, 'newname:' . $newName, 'dry_run:' . $dry_run]); - + if (blessed($request) && $request->getResult('overwritten_playlist_id') && !$params->{'overwrite'}) { - + $params->{'warning'} = 'RENAME_WARNING'; - + } - + else { - - my $request = Slim::Control::Request::executeRequest(undef, [ - 'playlists', - 'rename', + + my $request = Slim::Control::Request::executeRequest($client, [ + 'playlists', + 'rename', 'playlist_id:' . $playlist_id, 'newname:' . $newName]); - + if ($request && $request->getResult('writeError')) { - + $params->{'warning'} = $client->string('PLAYLIST_CANT_WRITE'); - + } } @@ -175,7 +175,7 @@ sub renamePlaylist { sub deletePlaylist { my ($client, $params) = @_; - + my $playlist_id = $params->{'playlist_id'}; my $playlistObj = Slim::Schema->find('Playlist', $playlist_id); @@ -191,9 +191,9 @@ sub deletePlaylist { ['playlists', 'delete', 'playlist_id:' . $playlist_id]); if ($request && $request->getResult('writeError')) { - + $params->{'warning'} = $client->string('PLAYLIST_CANT_WRITE'); - + } else { # don't show the playlist name field any more delete $params->{'playlist_id'}; @@ -213,7 +213,7 @@ sub browsePlaylists { my $allArgs = \@_; my @verbs = ('browselibrary', 'items', 'feedMode:1', 'mode:playlists'); - + my $callback = sub { my ($client, $feed) = @_; Slim::Web::XMLBrowser->handleWebIndex( { @@ -228,7 +228,7 @@ sub browsePlaylists { # execute CLI command my $proxiedRequest = Slim::Control::Request::executeRequest( $client, ['browselibrary', 'items', 'feedMode:1', 'mode:playlists'] ); - + # wrap async requests if ( $proxiedRequest->isStatusProcessing ) { $proxiedRequest->callbackFunction( sub { $callback->($client, $_[0]->getResults); } ); @@ -242,13 +242,13 @@ sub browsePlaylist { my $allArgs = \@_; my $playlist_id = $params->{'playlist.id'}; - + my $title; my $obj = Slim::Schema->find('Playlist', $playlist_id); $title = string('PLAYLIST') . ' (' . $obj->name . ')' if $obj; - + my @verbs = ('browselibrary', 'items', 'feedMode:1', 'mode:playlistTracks', 'playlist_id:' . $playlist_id); - + my $callback = sub { my ($client, $feed) = @_; Slim::Web::XMLBrowser->handleWebIndex( { @@ -265,7 +265,7 @@ sub browsePlaylist { # execute CLI command my $proxiedRequest = Slim::Control::Request::executeRequest( $client, \@verbs ); - + # wrap async requests if ( $proxiedRequest->isStatusProcessing ) { $proxiedRequest->callbackFunction( sub { $callback->($client, $_[0]->getResults); } ); diff --git a/Slim/Web/Pages/Home.pm b/Slim/Web/Pages/Home.pm index 1b22f8169e8..a87cb542755 100644 --- a/Slim/Web/Pages/Home.pm +++ b/Slim/Web/Pages/Home.pm @@ -1,8 +1,8 @@ package Slim::Web::Pages::Home; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -10,6 +10,7 @@ use strict; use Data::URIEncode qw(complex_to_query); use Digest::MD5 qw(md5_hex); use HTTP::Status qw(RC_MOVED_TEMPORARILY); +use JSON::XS::VersionOneAndTwo; use Slim::Utils::Cache; use Slim::Utils::Prefs; @@ -32,8 +33,8 @@ sub init { Slim::Web::Pages->addPageFunction(qr/^home\.(?:htm|xml)/, \&home); Slim::Web::Pages->addPageFunction(qr/^index\.(?:htm|xml)/, \&home); Slim::Web::Pages->addPageFunction(qr/^switchserver\.(?:htm|xml)/, \&switchServer); - Slim::Web::Pages->addPageFunction(qr/^updateinfo\.htm/, \&updateInfo); - + Slim::Web::Pages->addPageFunction(qr/^updateinfo\.(?:htm|json)/, \&updateInfo); + Slim::Web::Pages->addPageLinks('my_apps', {'PLUGIN_APP_GALLERY_MODULE_NAME' => Slim::Networking::SqueezeNetwork->url('/appgallery') }) if !main::NOMYSB; Slim::Web::Pages->addPageLinks("help", { 'HELP_REMOTE' => "html/docs/remote.html" }); @@ -54,7 +55,7 @@ sub home { my $template = $params->{"path"} =~ /home\.(htm|xml)/ ? 'home.html' : 'index.html'; my $checksum; - + # allow the setup wizard to be skipped in case the user's using an old browser (eg. Safari 1.x) if ($params->{skipWizard}) { $prefs->set('wizardDone', 1); @@ -63,7 +64,7 @@ sub home { } } - # redirect to the setup wizard if it has never been run before + # redirect to the setup wizard if it has never been run before if (!$prefs->get('wizardDone')) { $response->code(RC_MOVED_TEMPORARILY); $response->header('Location' => '/settings/server/wizard.html'); @@ -89,36 +90,36 @@ sub home { if ($client && Slim::Utils::PluginManager->isEnabled('Slim::Plugin::DigitalInput::Plugin')) { Slim::Plugin::DigitalInput::Plugin->webPages($client->hasDigitalIn); } - + # More leakage from the LineIn/Out 'plugins' # # If our current player has line, show the menu. if ($client && Slim::Utils::PluginManager->isEnabled('Slim::Plugin::LineIn::Plugin')) { Slim::Plugin::LineIn::Plugin->webPages($client); } - + if ($client && Slim::Utils::PluginManager->isEnabled('Slim::Plugin::LineOut::Plugin')) { Slim::Plugin::LineOut::Plugin->webPages($client); } - + if (my $favs = Slim::Utils::Favorites->new($client)) { $params->{'favorites'} = $favs->toplevel; } - + # Bug 4125, sort all additionalLinks submenus properly $params->{additionalLinkOrder} = {}; $params->{additionalLinks} = {}; - + # Get sort order for plugins my $pluginWeights = Slim::Plugin::Base->getWeights(); - + my $conditions = \%Slim::Web::Pages::pageConditions; - + my $cmpStrings = {}; while (my ($menu, $menuItems) = each %Slim::Web::Pages::additionalLinks ) { - + next if $menu eq 'apps' && !main::NOMYSB; - + $params->{additionalLinks}->{ $menu } = { map { $_ => $menuItems->{ $_ }; @@ -131,63 +132,63 @@ sub home { } keys %$menuItems }; - + $params->{additionalLinkOrder}->{ $menu } = [ sort { ( $menu !~ /(?:my_apps)/ && ( $pluginWeights->{$a} || $prefs->get("rank-$a") || 0 ) <=> ( $pluginWeights->{$b} || $prefs->get("rank-$b") || 0 ) ) - || + || ( !main::NOMYSB && $menu =~ /(?:my_apps)/ && $a eq 'PLUGIN_APP_GALLERY_MODULE_NAME' && -1 ) - || + || ( !main::NOMYSB && $menu =~ /(?:my_apps)/ && $b eq 'PLUGIN_APP_GALLERY_MODULE_NAME' ) - || + || ( ( $cmpStrings->{$a} ||= lc(Slim::Buttons::Home::cmpString($client, $a)) ) cmp ( $cmpStrings->{$b} ||= lc(Slim::Buttons::Home::cmpString($client, $b)) ) ) } keys %{ $params->{additionalLinks}->{ $menu } } ]; } - + if (main::NOMYSB) { $params->{additionalLinks}->{my_apps} = delete $params->{additionalLinks}->{apps}; $params->{additionalLinkOrder}->{my_apps} = delete $params->{additionalLinkOrder}->{apps}; } - + if ( !($params->{page} && $params->{page} eq 'help') ) { Slim::Web::Pages::Common->addPlayerList($client, $params); Slim::Web::Pages::Common->addLibraryStats($params, $client); } - + if ( my $library_id = Slim::Music::VirtualLibraries->getLibraryIdForClient($client) ) { $params->{library_id} = $library_id; $params->{library_name} = Slim::Music::VirtualLibraries->getNameForId($library_id, $client); } - + if (!main::NOBROWSECACHE && $template eq 'home.html') { $checksum = md5_hex(Slim::Utils::Unicode::utf8off(join(':', ($client ? $client->id : ''), - $params->{newVersion}, - $params->{newPlugins}, - $params->{hasLibrary}, - $prefs->get('langauge'), - $params->{library_id}, - complex_to_query($params->{additionalLinks}), - complex_to_query($params->{additionalLinkOrder}), + $params->{newVersion} || '', + $params->{newPlugins} || '', + $params->{hasLibrary} || '', + $prefs->get('language'), + $params->{library_id} || '', + complex_to_query($params->{additionalLinks} || {}), + complex_to_query($params->{additionalLinkOrder} || {}), complex_to_query($params->{cookies} || {}), complex_to_query($params->{favorites} || {}), - $params->{'skinOverride'} || $prefs->get('skin'), - $template, - $params->{song_count}, - $params->{album_count}, - $params->{artist_count}, + $params->{'skinOverride'} || $prefs->get('skin') || '', + $template || '', + $params->{song_count} || 0, + $params->{album_count} || 0, + $params->{artist_count} || 0, ))); - + if (my $cached = $cache->get($checksum)) { return $cached; } @@ -196,14 +197,14 @@ sub home { my $page = Slim::Web::HTTP::filltemplatefile($template, $params); $cache->set($checksum, $page, 3600) if $checksum && !main::NOBROWSECACHE; - + return $page; } sub updateInfo { my ($client, $params, $callback) = @_; - my $current = {}; + my ($current) = Slim::Plugin::Extensions::Plugin::getCurrentPlugins(); my $request = Slim::Control::Request->new(undef, ['appsquery']); @@ -212,7 +213,7 @@ sub updateInfo { details => 1, current => $current, }); - + $params->{pt} = { request => $request, }; @@ -225,19 +226,36 @@ sub updateInfo { sub _updateInfoCB { my ($client, $params, $callback, $httpClient, $response) = @_; - + my $request = $params->{pt}->{request}; - + $params->{'newVersion'} = $::newVersion; - + my $newPlugins = $request->getResult('updates') || {}; $params->{'newPlugins'} = $request && [ map { $_->{info} } grep { $_ } values %$newPlugins ]; - + if ($params->{installerFile}) { $params->{'newVersion'} = ${Slim::Web::HTTP::filltemplatefile('html/docs/linux-update.html', $params)}; } - - $callback->($client, $params, Slim::Web::HTTP::filltemplatefile('update_software.html', $params), $httpClient, $response); + + my $content; + if ($params->{path} =~ /\.json/) { + my $json = {}; + + if ($params->{'newVersion'}) { + $json->{server} = $params->{'newVersion'}; + } + if ($params->{'newPlugins'} && scalar @{$params->{'newPlugins'}}) { + $json->{plugins} = $params->{'newPlugins'}; + } + + $response->content_type('application/json'); + my $content = to_json($json); + $callback->($client, $params, \$content, $httpClient, $response); + } + else { + $callback->($client, $params, Slim::Web::HTTP::filltemplatefile('update_software.html', $params), $httpClient, $response); + } } sub switchServer { @@ -282,18 +300,18 @@ sub switchServer { NAME => Slim::Utils::Strings::string('SQUEEZENETWORK') }; } - + my @servers = keys %{Slim::Networking::Discovery::Server::getServerList()}; $params->{serverlist} = \@servers; } - + return Slim::Web::HTTP::filltemplatefile('switchserver.html', $params); } # Bug 7254, don't tell Ray to reconnect to SN unless it's known to be attached to the user's account sub _canSwitch { if (!main::NOMYSB) { my $client = shift; - + return ( ($client->deviceid != 7) || Slim::Networking::SqueezeNetwork::Players->is_known_player($client) ); } } diff --git a/Slim/Web/Pages/JS.pm b/Slim/Web/Pages/JS.pm new file mode 100644 index 00000000000..0d450a92dd5 --- /dev/null +++ b/Slim/Web/Pages/JS.pm @@ -0,0 +1,65 @@ +package Slim::Web::Pages::JS; + +# Logitech Media Server Copyright 2001-2020 Logitech. +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License, +# version 2. + +=pod + Under some cercumstances we want to add JavaScript to the main JS files + used in the main web UI. Those usually are handled without page handler. + But in order for a plugin to be able to add its JS we need some more + flexibility - given here. +=cut + +use strict; + +use Slim::Utils::Log; +use Slim::Web::Pages; + +my $log = logger('network.http'); +my (%handlers); + +sub addJSFunction { + my ( $class, $jsFile, $customJSFile ) = @_; + + $jsFile = lc($jsFile || ''); + $jsFile =~ s/\.html$//; + + if ( $jsFile && $customJSFile && $jsFile =~ /^js-?(?:main|browse)$/ ) { + if (!$handlers{$jsFile}) { + Slim::Web::Pages->addPageFunction("js-main\.html", \&handler); + $handlers{$jsFile} = []; + } + + push @{$handlers{$jsFile}}, $customJSFile; + } + else { + $log->warn("No or invalid JS template defined"); + } +} + +sub handler { + my ($client, $params, $callback, $httpClient, $response) = @_; + + my ($template) = $params->{path} =~ /(js.*?)\.html/; + + # let's render those templates to include them in the main JS file + # unfortunately we can't easily do this from the template itself + foreach ( @{$handlers{$template} || []} ) { + my $handler = Slim::Web::Pages->getPageFunction($_); + + if ($handler) { + $params->{additionalJS} ||= ''; + eval { + $params->{additionalJS} .= ${$handler->(@_)}; + }; + + $@ && $log->warn("Failed to render JS template: $@") + } + } + + Slim::Web::HTTP::filltemplatefile($params->{path}, $params); +} + +1; \ No newline at end of file diff --git a/Slim/Web/Pages/Playlist.pm b/Slim/Web/Pages/Playlist.pm index 41f8b85f5ce..8e6c8e9da14 100644 --- a/Slim/Web/Pages/Playlist.pm +++ b/Slim/Web/Pages/Playlist.pm @@ -1,6 +1,6 @@ package Slim::Web::Pages::Playlist; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -78,7 +78,7 @@ sub playlist { $cacheKey = join(':', $client->id, $client->currentPlaylistChangeTime(), - $prefs->get('langauge'), + $prefs->get('language'), $currentSkin, $params->{'start'}, $songcount, @@ -137,7 +137,7 @@ sub playlist { # get the playlist duration - use cached value if playlist has not changed my $durationCacheKey = 'playlist_duration_' . $client->currentPlaylistUpdateTime(); - if ( my $cached = $cache->get($durationCacheKey) ) { + if ( $songcount > 1 && (my $cached = $cache->get($durationCacheKey)) ) { $params->{'pageinfo'}->{'totalDuration'} = Slim::Utils::DateTime::timeFormat($cached); } else { @@ -180,7 +180,7 @@ sub playlist { # try to use cached data if we've been showing the same set of tracks before my $tracksCacheKey = join('_', 'playlist_tracks_', $client->currentPlaylistUpdateTime(), $start, $itemsPerPage, $tags); my $tracks; - if ( my $cached = $cache->get($tracksCacheKey) ) { + if ( $songcount > 1 && (my $cached = $cache->get($tracksCacheKey)) ) { $tracks = $cached; } else { diff --git a/Slim/Web/Pages/Search.pm b/Slim/Web/Pages/Search.pm index a76a674f90f..b9f3f8c2bec 100644 --- a/Slim/Web/Pages/Search.pm +++ b/Slim/Web/Pages/Search.pm @@ -1,8 +1,7 @@ package Slim::Web::Pages::Search; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -57,6 +56,7 @@ sub search { if ($params->{'ajaxSearch'}) { $params->{'itemsPerPage'} = MAXRESULTS; $params->{'path'} = "clixmlbrowser/clicmd=browselibrary+items&linktitle=SEARCH&mode=search$library_id/"; + $params->{'q'} =~ s/\*//g; return Slim::Web::XMLBrowser::webLink(@_); } @@ -129,6 +129,7 @@ sub advancedSearch { # keep a copy of the search params to be stored in a saved search my %searchParams; + my %joins; # Check for valid search terms for my $key (sort keys %$params) { @@ -164,11 +165,14 @@ sub advancedSearch { # Do the same for 'op's $params->{'search'}->{$newKey}->{'op'} = $params->{$key.'.op'}; - $newKey =~ s/_(rating|playcount)\b/\.$1/; + $newKey =~ s/_(rating|playcount|value|titlesearch|namesearch|)\b/\.$1/; # add these onto the query string. kinda jankey. push @qstring, join('=', "$key.op", $op); - push @qstring, join('=', $key, $params->{$key}); + + if (!grep /^$key=/, @qstring) { + push @qstring, join('=', $key, $params->{$key}); + } # Bitrate needs to changed a bit if ($key =~ /bitrate$/) { @@ -186,9 +190,32 @@ sub advancedSearch { splice(@{$params->{$key}}, 2); } - # Map the type to the query - # This will be handed to SQL::Abstract - $query{$newKey} = { $op => $params->{$key} }; + if ($op =~ /(NOT )?LIKE/) { + $op = $1 ? 'not like' : 'like'; + } + + if ($op =~ /STARTS (NOT )?WITH/) { + $op = $1 ? 'not like' : 'like'; + + # depending search preferences we might have an array, or even nested array - but we only want the first item + while (ref $params->{$key} eq 'ARRAY') { + $params->{$key} = shift @{$params->{$key}}; + } + + $params->{$key} =~ s/^\%//; + } + + # if we've got multiple arguments, we'll have to logically AND them in case of NOT LIKE + if ($op eq 'not like' && ref $params->{$key} eq 'ARRAY' && ref $params->{$key}->[0] eq 'ARRAY') { + $query{-and} ||= []; + push @{$query{-and}}, map { $newKey => { 'not like' => $_ }} @{$params->{$key}->[0]}; + delete $query{$newKey}; + } + else { + # Map the type to the query + # This will be handed to SQL::Abstract + $query{$newKey} = { $op => $params->{$key} }; + } # don't include null/0 value years in search for earlier years # http://bugs.slimdevices.com/show_bug.cgi?id=5713 @@ -228,9 +255,10 @@ sub advancedSearch { # # Turn the track_title into track.title for the query. # We need the _'s in the form, because . means hash key. - if ($newKey =~ s/_(titlesearch|namesearch|value)$/\.$1/) { + if ($newKey =~ s/(.+)_(titlesearch|namesearch|value|)$/$1\.$2/) { + $joins{$1}++ if $1 ne 'me'; - $params->{$key} = { 'like' => Slim::Utils::Text::searchStringSplit($params->{$key}) }; + $params->{$key} = Slim::Utils::Text::searchStringSplit($params->{$key}); } $newKey =~ s/_(rating|playcount)\b/\.$1/; @@ -238,7 +266,7 @@ sub advancedSearch { # Wildcard searches if ($newKey =~ /lyrics/) { - $params->{$key} = { 'like' => Slim::Utils::Text::searchStringSplit($params->{$key}) }; + $params->{$key} = Slim::Utils::Text::searchStringSplit($params->{$key}); } if ($newKey =~ /url/) { @@ -249,7 +277,7 @@ sub advancedSearch { # replace the % in the URI escaped string with a single character placeholder $uri =~ s/%/_/g; - $params->{$key} = { 'like' => "%$uri%" }; + $params->{$key} = "%$uri%"; } $query{$newKey} = $params->{$key}; @@ -326,7 +354,7 @@ sub advancedSearch { my @joins = (); _initActiveRoles($params); - if ($query{'contributor.namesearch'}) { + if ($query{'contributor.namesearch'} || $joins{'contributor'}) { if (keys %{$params->{'search'}->{'contributor_namesearch'}}) { my @roles; @@ -339,7 +367,7 @@ sub advancedSearch { $query{"contributorTracks.role"} = \@roles if @roles; } - if ($query{'contributor.namesearch'}) { + if ($query{'contributor.namesearch'} || $joins{'contributor'}) { push @joins, { "contributorTracks" => 'contributor' }; @@ -389,12 +417,12 @@ sub advancedSearch { $query{'me.audio'} = 1; - if ($query{'album.titlesearch'}) { + if ($query{'album.titlesearch'} || $joins{'album'}) { push @joins, 'album'; } - if ($query{'comments.value'}) { + if ($query{'comments.value'} || $joins{'comments'}) { push @joins, 'comments'; } diff --git a/Slim/Web/Pages/Status.pm b/Slim/Web/Pages/Status.pm index d0b75be14c6..69b96423ef4 100644 --- a/Slim/Web/Pages/Status.pm +++ b/Slim/Web/Pages/Status.pm @@ -1,8 +1,7 @@ package Slim::Web::Pages::Status; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Pages/Trackinfo.pm b/Slim/Web/Pages/Trackinfo.pm index dcb8ee18572..00e7e0aaef7 100644 --- a/Slim/Web/Pages/Trackinfo.pm +++ b/Slim/Web/Pages/Trackinfo.pm @@ -1,10 +1,8 @@ package Slim::Web::Pages::Trackinfo; -# $Id: Trackinfo.pm 30446 2010-03-31 12:11:29Z ayoung1 $ - -# Logitech Media Server Copyright 2003-2010 Logitech. +# Logitech Media Server Copyright 2003-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. use strict; @@ -23,22 +21,31 @@ sub init { sub trackinfo { my $client = shift; my $params = shift; - + my $id = $params->{sess} || $params->{item}; my $track = Slim::Schema->find( Track => $id ); - - my $menu = Slim::Menu::TrackInfo->menu( $client, $track->url, $track ) if $track; - + my $url = $track ? $track->url : ''; + + my $controller = $client->master->controller; + my $song = $controller->streamingSong; + + # let's try to get the real url for a remote stream + if ($song && $song->isRemote && $song->streamUrl eq $url && $song->track && ($song->track->url ne $url)) { + $url = $song->track->url + } + + my $menu = Slim::Menu::TrackInfo->menu( $client, $url, $track ) if $track; + # some additional parameters for the nice favorites button at the top - $params->{isFavorite} = defined Slim::Utils::Favorites->new($client)->findUrl($track->url); - $params->{itemUrl} = $track->url; + $params->{isFavorite} = defined Slim::Utils::Favorites->new($client)->findUrl($url); + $params->{itemUrl} = $url; # Pass-through track ID as sess param $params->{sess} = $id; - + # Include track cover image $params->{image} = $menu->{cover}; - + Slim::Web::XMLBrowser->handleWebIndex( { client => $client, path => 'trackinfo.html', diff --git a/Slim/Web/Settings.pm b/Slim/Web/Settings.pm index d02dce2f1a6..5aeeff91c69 100644 --- a/Slim/Web/Settings.pm +++ b/Slim/Web/Settings.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Player/Alarm.pm b/Slim/Web/Settings/Player/Alarm.pm index 0547d69fc1e..cf2f4fd2ee1 100644 --- a/Slim/Web/Settings/Player/Alarm.pm +++ b/Slim/Web/Settings/Player/Alarm.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Player::Alarm; -# $Id: Basic.pm 10633 2006-11-09 04:26:27Z kdf $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Player/Audio.pm b/Slim/Web/Settings/Player/Audio.pm index f7fae62fb15..11f658c6434 100644 --- a/Slim/Web/Settings/Player/Audio.pm +++ b/Slim/Web/Settings/Player/Audio.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Player::Audio; -# $Id: Basic.pm 10633 2006-11-09 04:26:27Z kdf $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -31,12 +29,12 @@ sub needsClient { sub prefs { my ($class, $client) = @_; - my @prefs = qw(powerOnResume lameQuality maxBitrate); + my @prefs = qw(powerOnResume lameQuality maxBitrate fadeInDuration); if ($client->hasPowerControl()) { push @prefs,'powerOffDac'; } - + if ($client->hasDisableDac()) { push @prefs,'disableDac'; } @@ -44,7 +42,7 @@ sub prefs { if ($client->maxTransitionDuration()) { push @prefs,qw(transitionType transitionDuration transitionSmart transitionSampleRestriction); } - + if ($client->hasDigitalOut()) { push @prefs,qw(digitalVolumeControl mp3SilencePrelude); } @@ -52,7 +50,7 @@ sub prefs { if ($client->hasPreAmp()) { push @prefs,'preampVolumeControl'; } - + if ($client->hasAesbeu()) { push @prefs,'digitalOutputEncoding'; } @@ -72,23 +70,23 @@ sub prefs { if ($client->hasPolarityInversion()) { push @prefs,'polarityInversion'; } - + if ($client->hasDigitalIn()) { push @prefs,'wordClockOutput'; } - + if ($client->hasRolloff()) { push @prefs, 'rolloffSlow'; } - + if ($client->canDoReplayGain(0)) { push @prefs, 'replayGainMode', 'remoteReplayGain'; } - + if ($client->hasHeadSubOut()) { push @prefs, 'analogOutMode'; } - + if ($client->maxBass() - $client->minBass() > 0) { push @prefs, 'bass'; } @@ -96,19 +94,19 @@ sub prefs { if ($client->maxTreble() - $client->minTreble() > 0) { push @prefs, 'treble'; } - + if ($client->maxXL() - $client->minXL()) { push @prefs, 'stereoxl'; } - + if ($client->can('setLineIn') && Slim::Utils::PluginManager->isEnabled('Slim::Plugin::LineIn::Plugin')) { push @prefs, 'lineInLevel', 'lineInAlwaysOn'; } - + if ( $client->isa('Slim::Player::Squeezebox2') ) { push @prefs, 'mp3StreamingMethod'; } - + if ($client->hasOutputChannels()) { push @prefs, 'outputChannels'; } @@ -122,13 +120,18 @@ sub beforeRender { # Load any option lists for dynamic options. $paramRef->{'lamefound'} = Slim::Utils::Misc::findbin('lame'); - + my @formats = $client->formats(); if ($formats[0] ne 'mp3') { $paramRef->{'allowNoLimit'} = 1; } + $paramRef->{'maxTreble'} = $client->maxTreble; + $paramRef->{'minTreble'} = $client->minTreble; + $paramRef->{'maxBass'} = $client->maxBass; + $paramRef->{'minBass'} = $client->minBass; + $paramRef->{'prefs'}->{pref_maxBitrate} = Slim::Utils::Prefs::maxRate($client, 1); } diff --git a/Slim/Web/Settings/Player/Basic.pm b/Slim/Web/Settings/Player/Basic.pm index 04e7a0b067a..61ee1d366ed 100644 --- a/Slim/Web/Settings/Player/Basic.pm +++ b/Slim/Web/Settings/Player/Basic.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Player::Basic; -# $Id: Basic.pm 10633 2006-11-09 04:26:27Z kdf $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Player/Display.pm b/Slim/Web/Settings/Player/Display.pm index 2e7251dbc22..9e0d92a7fe7 100644 --- a/Slim/Web/Settings/Player/Display.pm +++ b/Slim/Web/Settings/Player/Display.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Player::Display; -# $Id: Basic.pm 10633 2006-11-09 04:26:27Z kdf $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Player/Menu.pm b/Slim/Web/Settings/Player/Menu.pm index 6176fdeca2b..0d8fee021e9 100644 --- a/Slim/Web/Settings/Player/Menu.pm +++ b/Slim/Web/Settings/Player/Menu.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Player::Menu; -# $Id: Basic.pm 10633 2006-11-09 04:26:27Z kdf $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Player/Remote.pm b/Slim/Web/Settings/Player/Remote.pm index cb674d8ae39..76e0c13c44b 100644 --- a/Slim/Web/Settings/Player/Remote.pm +++ b/Slim/Web/Settings/Player/Remote.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Player::Remote; -# $Id: Basic.pm 10633 2006-11-09 04:26:27Z kdf $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Player/Synchronization.pm b/Slim/Web/Settings/Player/Synchronization.pm index f63ff856fce..08975f04849 100644 --- a/Slim/Web/Settings/Player/Synchronization.pm +++ b/Slim/Web/Settings/Player/Synchronization.pm @@ -1,6 +1,6 @@ package Slim::Web::Settings::Player::Synchronization; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/Basic.pm b/Slim/Web/Settings/Server/Basic.pm index 65324aa72e8..e7b8595aed4 100644 --- a/Slim/Web/Settings/Server/Basic.pm +++ b/Slim/Web/Settings/Server/Basic.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::Basic; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -29,7 +28,7 @@ sub prefs { sub handler { my ($class, $client, $paramRef) = @_; - + # tell the server not to trigger a rescan immediately, but let it queue up requests # this is neede to prevent multiple scans to be triggered by change handlers for paths etc. Slim::Music::Import->doQueueScanTasks(1); @@ -53,9 +52,9 @@ sub handler { my (undef, $ok) = $prefs->set($pref, $paramRef->{"pref_$pref"}); if ($ok) { - $paramRef->{'validated'}->{$pref} = 1; + $paramRef->{'validated'}->{$pref} = 1; } - else { + else { $paramRef->{'warning'} .= sprintf(Slim::Utils::Strings::string('SETTINGS_INVALIDVALUE'), $paramRef->{"pref_$pref"}, $pref) . '
'; $paramRef->{'validated'}->{$pref} = 0; } @@ -68,7 +67,7 @@ sub handler { Slim::Control::Request::executeRequest(undef, $rescanType); $runScan = 1; } - + if ( $paramRef->{'saveSettings'} ) { my $curLang = $prefs->get('language'); my $lang = $paramRef->{'pref_language'}; @@ -78,14 +77,14 @@ sub handler { if ($lang eq 'HE' && $prefs->get('skin') eq 'Default') { $prefs->set('skin', 'Classic'); $paramRef->{'warning'} .= '' . Slim::Utils::Strings::string("SETUP_SKIN_OK") . ''; - } + } # Bug 5740, flush the playlist cache for my $client (Slim::Player::Client::clients()) { $client->currentPlaylistChangeTime(Time::HiRes::time()); } } - + # handle paths my @paths; my %oldPaths = map { $_ => 1 } @{ $prefs->get('mediadirs') || [] }; @@ -105,19 +104,19 @@ sub handler { if ($paramRef->{"pref_rescan_mediadir$i"}) { $singleDirScan = Slim::Utils::Misc::fileURLFromPath($path); } - + push @{ $ignoreFolders->{audio} }, $path if !$paramRef->{"pref_ignoreInAudioScan$i"}; push @{ $ignoreFolders->{video} }, $path if !$paramRef->{"pref_ignoreInVideoScan$i"}; push @{ $ignoreFolders->{image} }, $path if !$paramRef->{"pref_ignoreInImageScan$i"}; } } - + $prefs->set('ignoreInAudioScan', $ignoreFolders->{audio}); $prefs->set('ignoreInVideoScan', $ignoreFolders->{video}); $prefs->set('ignoreInImageScan', $ignoreFolders->{image}); my $oldCount = scalar @{ $prefs->get('mediadirs') || [] }; - + if ( keys %oldPaths || !$oldCount || scalar @paths != $oldCount ) { $prefs->set('mediadirs', \@paths); } @@ -130,15 +129,15 @@ sub handler { $paramRef->{'newVersion'} = $::newVersion; $paramRef->{'languageoptions'} = Slim::Utils::Strings::languageOptions(); - + my $ignoreFolders = { - audio => { map { $_, 1 } @{ Slim::Utils::Misc::getDirsPref('ignoreInAudioScan') } }, - video => { map { $_, 1 } @{ Slim::Utils::Misc::getDirsPref('ignoreInVideoScan') } }, - image => { map { $_, 1 } @{ Slim::Utils::Misc::getDirsPref('ignoreInImageScan') } }, + audio => { map { $_, 1 } @{ $prefs->get('ignoreInAudioScan') || [''] } }, + video => { map { $_, 1 } @{ $prefs->get('ignoreInVideoScan') || [''] } }, + image => { map { $_, 1 } @{ $prefs->get('ignoreInImageScan') || [''] } }, }; - + $paramRef->{mediadirs} = []; - foreach ( @{ Slim::Utils::Misc::getMediaDirs() } ) { + foreach ( @{ $prefs->get('mediadirs') || [''] } ) { push @{ $paramRef->{mediadirs} }, { path => $_, audio => $ignoreFolders->{audio}->{$_}, diff --git a/Slim/Web/Settings/Server/Behavior.pm b/Slim/Web/Settings/Server/Behavior.pm index 1489abd4c6e..45f8329a517 100644 --- a/Slim/Web/Settings/Server/Behavior.pm +++ b/Slim/Web/Settings/Server/Behavior.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::Behavior; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/Debugging.pm b/Slim/Web/Settings/Server/Debugging.pm index bd70a10d998..5bea17effd6 100644 --- a/Slim/Web/Settings/Server/Debugging.pm +++ b/Slim/Web/Settings/Server/Debugging.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::Debugging; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/FileSelector.pm b/Slim/Web/Settings/Server/FileSelector.pm index 4b00bf84e2d..b0237c1b0f4 100644 --- a/Slim/Web/Settings/Server/FileSelector.pm +++ b/Slim/Web/Settings/Server/FileSelector.pm @@ -1,6 +1,6 @@ package Slim::Web::Settings::Server::FileSelector; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/FileTypes.pm b/Slim/Web/Settings/Server/FileTypes.pm index 5494a4f3e5c..d7d82957ed6 100644 --- a/Slim/Web/Settings/Server/FileTypes.pm +++ b/Slim/Web/Settings/Server/FileTypes.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::FileTypes; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -45,6 +44,7 @@ sub handler { # If the conversion pref is enabled confirm that # it's allowed to be checked. + $paramRef->{$profile} ||= ''; if ($paramRef->{$profile} ne 'DISABLED' && $disabledformats{$profile}) { if (!Slim::Player::TranscodingHelper::checkBin($profile,'IgnorePrefs')) { @@ -96,10 +96,11 @@ sub handler { $binstring = undef; } } + + $binstring ||= ''; }iegsx; - if (defined $binstring && $binstring ne '-') { - + if ($binstring && $binstring ne '-') { push @binaries, $binstring; } elsif ($cmdline eq '-' || $binstring eq '-') { diff --git a/Slim/Web/Settings/Server/Index.pm b/Slim/Web/Settings/Server/Index.pm index 8ceccf9b61a..61b80c11e68 100644 --- a/Slim/Web/Settings/Server/Index.pm +++ b/Slim/Web/Settings/Server/Index.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Server::Index; -# $Id: UserInterface.pm 13299 2007-09-27 08:59:36Z mherger $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/Network.pm b/Slim/Web/Settings/Server/Network.pm index ee868148732..e5e833ccf49 100644 --- a/Slim/Web/Settings/Server/Network.pm +++ b/Slim/Web/Settings/Server/Network.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::Network; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/Performance.pm b/Slim/Web/Settings/Server/Performance.pm index 12b03c782ea..d6aed3ecbd2 100644 --- a/Slim/Web/Settings/Server/Performance.pm +++ b/Slim/Web/Settings/Server/Performance.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::Performance; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -31,7 +30,7 @@ sub prefs { sub handler { my ($class, $client, $paramRef, $pageSetup) = @_; - + if ( $paramRef->{'saveSettings'} ) { my $curAuto = $prefs->get('autorescan'); if ( $curAuto != $paramRef->{pref_autorescan} ) { @@ -43,37 +42,37 @@ sub handler { Slim::Utils::AutoRescan->shutdown; } } - + my $specs = Storable::dclone($prefs->get('customArtSpecs')); - + my @delete = @{ ref $paramRef->{delete} eq 'ARRAY' ? $paramRef->{delete} : [ $paramRef->{delete} ] }; - + for my $deleteItem (@delete) { delete $specs->{$deleteItem}; } - + $prefs->set( customArtSpecs => $specs ); - + } - + # Restart message if dbhighmem is changed my $curmem = $prefs->get('dbhighmem') || 0; if ( $paramRef->{pref_dbhighmem} && $paramRef->{pref_dbhighmem} != $curmem ) { # Trigger restart required message $paramRef = Slim::Web::Settings::Server::Plugins->getRestartMessage($paramRef, Slim::Utils::Strings::string('CLEANUP_PLEASE_RESTART_SC')); } - + # Restart if restart=1 param is set if ( $paramRef->{restart} ) { $paramRef = Slim::Web::Settings::Server::Plugins->restartServer($paramRef, 1); } - + $paramRef->{imageproxies} = { 1 => Slim::Utils::Strings::string('SETUP_IMAGEPROXY_LOCAL'), }; - + $paramRef->{imageproxies}->{0} = Slim::Utils::Strings::string('SETUP_IMAGEPROXY_REMOTE') unless main::NOMYSB; - + my $externalImageProxies = Slim::Web::ImageProxy->getExternalHandlers(); foreach (keys %$externalImageProxies) { $paramRef->{imageproxies}->{$_} = $externalImageProxies->{$_}->{desc}; @@ -89,8 +88,9 @@ sub handler { 15 => 'SETUP_PRIORITY_LOW' }->{$_} } (-20 .. 20) }; - + $paramRef->{pref_customArtSpecs} = $prefs->get('customArtSpecs'); + $paramRef->{prioritySettings} = defined Slim::Utils::OSDetect::getOS->getPriority() ? 1 : 0; return $class->SUPER::handler($client, $paramRef, $pageSetup); } diff --git a/Slim/Web/Settings/Server/Plugins.pm b/Slim/Web/Settings/Server/Plugins.pm index f797d697675..5f2de3ea2a0 100644 --- a/Slim/Web/Settings/Server/Plugins.pm +++ b/Slim/Web/Settings/Server/Plugins.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::Plugins; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -23,64 +22,6 @@ sub page { return Slim::Web::HTTP::CSRF->protectURI('settings/server/plugins.html'); } -=pod -# XXX - don't need this any more, as the Extensions plugin is enforced? -sub handler { - my ($class, $client, $paramRef) = @_; - - my $plugins = Slim::Utils::PluginManager->allPlugins; - my $pluginState = preferences('plugin.state')->all(); - - for my $plugin (keys %{$plugins}) { - - my $name = $plugins->{$plugin}->{'name'}; - - $plugins->{$plugin}->{errorDesc} = Slim::Utils::PluginManager->getErrorString($plugin); - - if ( $paramRef->{'saveSettings'} ) { - - next if $plugins->{$plugin}->{'enforce'}; - - if (!$paramRef->{$name} && $pluginState->{$plugin} eq 'enabled') { - Slim::Utils::PluginManager->disablePlugin($plugin); - } - - if ($paramRef->{$name} && $pluginState->{$plugin} eq 'disabled') { - Slim::Utils::PluginManager->enablePlugin($plugin); - } - } - - } - - if (Slim::Utils::PluginManager->needsRestart) { - - $paramRef = $class->getRestartMessage($paramRef, Slim::Utils::Strings::string('PLUGINS_CHANGED')); - } - - $paramRef = $class->restartServer($paramRef, Slim::Utils::PluginManager->needsRestart); - - $paramRef->{plugins} = $plugins; - $paramRef->{failsafe} = $main::failsafe; - - $paramRef->{pluginState} = preferences('plugin.state')->all(); - - # FIXME: temp remap new states to binary value: - for my $plugin (keys %{$paramRef->{pluginState}}) { - $paramRef->{pluginState}->{$plugin} = $paramRef->{pluginState}->{$plugin} =~ /enabled/; - } - - my @sortedPlugins = - map { $_->[1] } - sort { $a->[0] cmp $b->[0] } - map { [ uc( Slim::Utils::Strings::getString($plugins->{$_}->{name}) ), $_ ] } - keys %$plugins; - - $paramRef->{sortedPlugins} = \@sortedPlugins; - - return $class->SUPER::handler($client, $paramRef); -} -=cut - sub getRestartMessage { my ($class, $paramRef, $noRestartMsg) = @_; diff --git a/Slim/Web/Settings/Server/Security.pm b/Slim/Web/Settings/Server/Security.pm index b598e5a15f7..2ccf3359a5c 100644 --- a/Slim/Web/Settings/Server/Security.pm +++ b/Slim/Web/Settings/Server/Security.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::Security; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -24,7 +23,7 @@ sub page { } sub prefs { - return (preferences('server'), qw(filterHosts allowedHosts csrfProtectionLevel authorize username) ); + return (preferences('server'), qw(filterHosts allowedHosts corsAllowedHosts csrfProtectionLevel authorize username) ); } sub handler { @@ -32,10 +31,10 @@ sub handler { # disable authorization if no username is set if ($paramRef->{'saveSettings'} && $paramRef->{'pref_authorize'} && !$paramRef->{'pref_username'}) { - + $paramRef->{'warning'} .= Slim::Utils::Strings::string('SETUP_MISSING_USERNAME') . ' '; $paramRef->{'pref_authorize'} = 0; - + } # pre-process password to avoid saving clear text @@ -53,11 +52,11 @@ sub handler { else { my $currentPassword = preferences('server')->get('password'); - + if (defined($val) && $val ne '' && ($currentPassword eq '' || sha1_base64($val) ne $currentPassword)) { $prefs->set('password', sha1_base64($val)); } - + } } diff --git a/Slim/Web/Settings/Server/Software.pm b/Slim/Web/Settings/Server/Software.pm index 5fcdc0250b3..6459dfb6418 100644 --- a/Slim/Web/Settings/Server/Software.pm +++ b/Slim/Web/Settings/Server/Software.pm @@ -1,6 +1,6 @@ package Slim::Web::Settings::Server::Software; -# Logitech Media Server Copyright 2001-2016 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/SqueezeNetwork.pm b/Slim/Web/Settings/Server/SqueezeNetwork.pm index 8777f34ffa1..42cb4141201 100644 --- a/Slim/Web/Settings/Server/SqueezeNetwork.pm +++ b/Slim/Web/Settings/Server/SqueezeNetwork.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::SqueezeNetwork; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -40,56 +39,58 @@ sub handler { # The hostname for mysqueezebox.com my $sn_server = Slim::Networking::SqueezeNetwork->get_server("sn"); $params->{sn_server} = $sn_server; - + $params->{prefs}->{pref_sn_email} = $prefs->get('sn_email'); $params->{prefs}->{pref_sn_sync} = $prefs->get('sn_sync'); if ( $params->{saveSettings} ) { - + if ( defined $params->{pref_sn_sync} ) { $prefs->set( 'sn_sync', $params->{pref_sn_sync} ); if ( UNIVERSAL::can('Slim::Networking::SqueezeNetwork::PrefSync', 'shutdown') ) { Slim::Networking::SqueezeNetwork::PrefSync->shutdown(); } - + if ( $params->{pref_sn_sync} ) { require Slim::Networking::SqueezeNetwork::PrefSync; Slim::Networking::SqueezeNetwork::PrefSync->init(); } - + $params->{prefs}->{pref_sn_sync} = $params->{pref_sn_sync}; } # set credentials if mail changed or a password is defined and it has changed + $params->{pref_sn_password_sha} = Slim::Utils::Unicode::utf8encode($params->{pref_sn_password_sha}) if $params->{pref_sn_password_sha}; + if ( $params->{pref_sn_email} ne $params->{prefs}->{pref_sn_email} || ( $params->{pref_sn_password_sha} && sha1_base64($params->{pref_sn_password_sha}) ne $prefs->get('sn_password_sha') ) ) { - + # Verify username/password Slim::Control::Request::executeRequest( $client, - [ - 'setsncredentials', - $params->{pref_sn_email}, + [ + 'setsncredentials', + $params->{pref_sn_email}, $params->{pref_sn_password_sha}, ], sub { my $request = shift; - + my $validated = $request->getResult('validated'); my $warning = $request->getResult('warning'); $params->{prefs}->{pref_sn_email} = $prefs->get('sn_email'); - + if ($params->{'AJAX'}) { $params->{'warning'} = $warning; $params->{'validated'}->{'valid'} = $validated; } - + if (!$validated) { - + $params->{'warning'} .= $warning . '
' unless $params->{'AJAX'}; - + $params->{prefs}->{pref_sn_email} = $params->{pref_sn_email}; delete $params->{pref_sn_email}; diff --git a/Slim/Web/Settings/Server/Status.pm b/Slim/Web/Settings/Server/Status.pm index 83a04be1adc..d457bf472b8 100644 --- a/Slim/Web/Settings/Server/Status.pm +++ b/Slim/Web/Settings/Server/Status.pm @@ -1,8 +1,6 @@ package Slim::Web::Settings::Server::Status; -# $Id: Basic.pm 13299 2007-09-27 08:59:36Z mherger $ - -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/TextFormatting.pm b/Slim/Web/Settings/Server/TextFormatting.pm index 3a385af773b..0043fbdf837 100644 --- a/Slim/Web/Settings/Server/TextFormatting.pm +++ b/Slim/Web/Settings/Server/TextFormatting.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::TextFormatting; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/UserInterface.pm b/Slim/Web/Settings/Server/UserInterface.pm index 13577d26b5d..6fc4a02e596 100644 --- a/Slim/Web/Settings/Server/UserInterface.pm +++ b/Slim/Web/Settings/Server/UserInterface.pm @@ -1,8 +1,7 @@ package Slim::Web::Settings::Server::UserInterface; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Settings/Server/Wizard.pm b/Slim/Web/Settings/Server/Wizard.pm index f25b84928ab..b4a4bdddfbd 100644 --- a/Slim/Web/Settings/Server/Wizard.pm +++ b/Slim/Web/Settings/Server/Wizard.pm @@ -1,6 +1,6 @@ package Slim::Web::Settings::Server::Wizard; -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Setup.pm b/Slim/Web/Setup.pm index f0b9d3b14fb..675e7bd9602 100644 --- a/Slim/Web/Setup.pm +++ b/Slim/Web/Setup.pm @@ -1,8 +1,7 @@ package Slim::Web::Setup; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Template/Context.pm b/Slim/Web/Template/Context.pm index 6f91bd49ec0..108ef893a0e 100644 --- a/Slim/Web/Template/Context.pm +++ b/Slim/Web/Template/Context.pm @@ -1,8 +1,7 @@ package Slim::Web::Template::Context; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Template/NoWeb.pm b/Slim/Web/Template/NoWeb.pm index 05586795e12..a15b4cf5a4f 100644 --- a/Slim/Web/Template/NoWeb.pm +++ b/Slim/Web/Template/NoWeb.pm @@ -1,8 +1,7 @@ package Slim::Web::Template::NoWeb; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. diff --git a/Slim/Web/Template/SkinManager.pm b/Slim/Web/Template/SkinManager.pm index c82c681e2c9..ae7bd92041e 100644 --- a/Slim/Web/Template/SkinManager.pm +++ b/Slim/Web/Template/SkinManager.pm @@ -1,16 +1,17 @@ package Slim::Web::Template::SkinManager; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License, +# modify it under the terms of the GNU General Public License, # version 2. -use base qw(Slim::Web::Template::NoWeb); +use base qw(Slim::Web::Template::NoWeb); use strict; +use File::Slurp; use File::Spec::Functions qw(:ALL); +use Digest::MD5 qw(md5_hex); use Template; use URI::Escape qw(uri_escape); use YAML::XS; @@ -28,7 +29,8 @@ BEGIN { $Template::Config::CONTEXT = 'Slim::Web::Template::Context'; # Use Profiler instead if you want to investigate page rendering performance # $Template::Config::CONTEXT = 'Slim::Web::Template::Profiler'; - $Template::Provider::MAX_DIRS = 128; + $Template::Provider::MAX_DIRS = 256; + $Template::Directive::WHILE_MAX = 10000; } use constant baseSkin => 'EN'; @@ -61,12 +63,12 @@ sub new { }; bless $self, $class; - + push @{ $self->{templateDirs} }, Slim::Utils::OSDetect::dirsFor('HTML'); - + my %skins = $self->skins(); $self->{skins} = \%skins; - + return $self; } @@ -86,7 +88,7 @@ sub isaSkin { sub skins { my $class = shift; - + # create a hash of available skins - used for skin override and by settings page my $UI = shift; # return format for settings page rather than lookup cache for skins @@ -104,15 +106,15 @@ sub skins { main::INFOLOG && $log->is_info && $log->info("skin entry: $dir"); if ($UI) { - + $dir = Slim::Utils::Misc::unescape($dir); my $name = Slim::Utils::Strings::getString( uc($dir) . '_SKIN' ); - + $skinlist{ $UI ? $dir : uc $dir } = ($name eq uc($dir) . '_SKIN') ? $dir : $name; } - + else { - + $skinlist{ uc $dir } = $dir; } } @@ -133,7 +135,7 @@ sub addSkinTemplate { my @skinParents = (); my @preprocess = qw(hreftemplate cmdwrappers); my $skinSettings = ''; - + for my $rootDir ($class->HTMLTemplateDirs()) { my $skinConfig = catfile($rootDir, $skin, 'skinconfig.yml'); @@ -176,7 +178,7 @@ sub addSkinTemplate { } } } - + if (ref($skinSettings) eq 'HASH' && ref $skinSettings->{'preprocess'} eq "ARRAY") { for my $checkfile (@{$skinSettings->{'preprocess'}}) { @@ -224,7 +226,7 @@ sub addSkinTemplate { 'utf8encode' => \&Slim::Utils::Unicode::utf8encode, 'utf8on' => \&Slim::Utils::Unicode::utf8on, 'utf8off' => \&Slim::Utils::Unicode::utf8off, - 'resizeimage' => [ \&_resizeImage, 1 ], + 'resizeimage' => [ \&_resizeImage, 1 ], 'imageproxy' => [ sub { return _resizeImage($_[0], $_[1], $_[2], '-'); }, 1 ], @@ -232,15 +234,21 @@ sub addSkinTemplate { EVAL_PERL => 1, ABSOLUTE => 1, - + # we usually don't change templates while running STAT_TTL => main::NOBROWSECACHE ? 1 : 3600, - + VARIABLES => { hasMediaSupport => main::IMAGE && main::MEDIASUPPORT, }, }); + my $versionFile = catfile($class->templateCacheDir(), md5_hex("$::VERSION/$::REVISION")); + if (-d $class->templateCacheDir() && !-f $versionFile) { + unlink map { catdir($class->templateCacheDir(), $_) } File::Slurp::read_dir($class->templateCacheDir()); + write_file($versionFile, ''); + } + return $class->{skinTemplates}->{$skin}; } @@ -256,36 +264,36 @@ sub _nonBreaking { sub _resizeImage { my ( $context, $width, $height, $mode, $prefix ) = @_; - + $height ||= ''; $mode ||= ''; $prefix ||= '/'; - + return sub { my $url = shift; # use local imageproxy to resize image (if enabled) $url = Slim::Web::ImageProxy::proxiedImage($url); - + my ($host) = Slim::Utils::Misc::crackURL($url); # don't use imageproxy on local network - if ( $host && Slim::Utils::Network::ip_is_private($host) || $host =~ /localhost/i ) { + if ( $host && (Slim::Utils::Network::ip_is_private($host) || $host =~ /localhost/i) ) { return $url; } - + # fall back to using external image proxy for external resources elsif ( !main::NOMYSB && $url =~ m{^https?://} ) { return Slim::Networking::SqueezeNetwork->url( "/public/imageproxy?w=$width&h=$height&u=" . uri_escape($url) ); } - + # $url comes with resizing parameters if ( $url =~ /_((?:[0-9X]+x[0-9X]+)(?:_\w)?(?:_[\da-fA-F]+)?(?:\.\w+)?)$/ ) { return $url; } - + # sometimes we'll need to prepend the webroot to our url $url = $prefix . $url unless $url =~ m{^/}; @@ -298,7 +306,7 @@ sub _resizeImage { if ( $url =~ m{^((?:$webroot|/)music/.*/cover)(?:\.jpg)?$} || $url =~ m{(.*imageproxy/.*/image)(?:\.(jpe?g|png|gif))} ) { return $1 . $resizeParams . '_o'; } - + # special mode "-": don't resize local urls (some already come with resize parameters) if ($mode eq '-') { if ($url =~ m|/[a-z]+\.png$|) { @@ -310,11 +318,11 @@ sub _resizeImage { } $resizeParams .= "_$mode" if $mode; - + $url =~ s/(\.png|\.gif|\.jpe?g|)$/$resizeParams$1/i; $url = '/' . $url unless $url =~ m{^(?:/|http)}; - - return $url; + + return $url; }; } @@ -322,7 +330,7 @@ sub _resizeImage { my %empty; sub _fillTemplate { my ($class, $params, $path, $skin) = @_; - + # Make sure we have a skin template for fixHttpPath to use. my $template = $class->{skinTemplates}->{$skin} || $class->addSkinTemplate($skin); @@ -331,8 +339,8 @@ sub _fillTemplate { $params->{'LOCALE'} = 'utf-8'; $path = $class->fixHttpPath($skin, $path); - - return \'' if $empty{$path}; + + return \'' if $empty{$path}; if (!$template->process($path, $params, \$output)) { @@ -365,11 +373,11 @@ Attempts to figure out what the browser is by user-agent string identification sub detectBrowser { my ($class, $request) = @_; - + my $return = 'unknown'; - + my $ua = $request->header('user-agent') || return $return; - + if ($ua =~ /Firefox/) { $return = 'Firefox'; } elsif ($ua =~ /Opera/) { @@ -388,7 +396,7 @@ sub detectBrowser { { $return = 'IE'; } - + return $return; } diff --git a/Slim/Web/XMLBrowser.pm b/Slim/Web/XMLBrowser.pm index dcc5140ca70..e97f7420fe0 100644 --- a/Slim/Web/XMLBrowser.pm +++ b/Slim/Web/XMLBrowser.pm @@ -1,8 +1,7 @@ package Slim::Web::XMLBrowser; -# $Id$ -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -286,7 +285,7 @@ sub handleFeed { $superFeed->{offset} ||= 0; main::DEBUGLOG && $log->is_debug && $log->debug("Considering $i=$in ($crumbText) from ", $stash->{'index'}, ' offset=', $superFeed->{'offset'}); - my $crumbName = $subFeed->{'name'} || $subFeed->{'title'}; + my $crumbName = Slim::Control::XMLBrowser::getTitle($subFeed->{name}, $subFeed); # Add search query to crumb list my $searchQuery; @@ -321,6 +320,7 @@ sub handleFeed { if ( $subFeed->{'play'} && $depth == $levels + && $stash->{'action'} && $stash->{'action'} =~ /^(?:play|add|insert)$/ ) { $subFeed->{'type'} = 'audio'; @@ -331,6 +331,7 @@ sub handleFeed { if ( $subFeed->{'playlist'} && $depth == $levels + && $stash->{'action'} && $stash->{'action'} =~ /^(?:playall|addall|insert|remove)$/ ) { $subFeed->{'type'} = 'playlist'; @@ -487,7 +488,7 @@ sub handleFeed { my $pt = $subFeed->{'passthrough'} || [undef]; my $search; - if ($searchQuery && $subFeed->{type} && $subFeed->{type} eq 'search') { + if (defined $searchQuery && $searchQuery ne '' && $subFeed->{type} && $subFeed->{type} eq 'search') { $search = $searchQuery; } @@ -501,7 +502,7 @@ sub handleFeed { if ($depth == $levels) { $args{'index'} = $index; - $args{'quantity'} = $stash->{'itemsPerPage'} || ($stash->{action} =~ /^(?:play|add)all$/i && $prefs->get('maxPlaylistLength')) || $prefs->get('itemsPerPage'); + $args{'quantity'} = $stash->{'itemsPerPage'} || ($stash->{action} && $stash->{action} =~ /^(?:play|add)all$/i && $prefs->get('maxPlaylistLength')) || $prefs->get('itemsPerPage'); } elsif ($depth < $levels) { $args{'index'} = $index[$depth]; $args{'quantity'} = 1; @@ -552,7 +553,7 @@ sub handleFeed { } $itemIndex .= '.'; - $stash->{'pagetitle'} = $subFeed->{'name'} || $subFeed->{'title'}; + $stash->{'pagetitle'} = Slim::Control::XMLBrowser::getTitle($subFeed->{'name'}, $subFeed); $stash->{'index'} = $itemIndex; $stash->{'icon'} = $subFeed->{'icon'}; $stash->{'playUrl'} = $subFeed->{'play'} @@ -601,7 +602,7 @@ sub handleFeed { # Play of a playlist should be playall if ($action - && ($streamItem ? $streamItem->{'type'} eq 'playlist' + && ($streamItem && $streamItem->{'type'} ? $streamItem->{'type'} eq 'playlist' : $stash->{'type'} && $stash->{'type'} eq 'playlist') && $action =~ /^(?:play|add)$/ ) { @@ -654,7 +655,7 @@ sub handleFeed { # XXX: Why is $stash->{streaminfo}->{item} added on here, it seems to be undef? for my $item ( @{ $stash->{'items'} }, $streamItem ) { my $url; - if ( $item->{'type'} eq 'audio' && $item->{'url'} ) { + if ( $item->{'type'} && $item->{'type'} eq 'audio' && $item->{'url'} ) { $url = $item->{'url'}; } elsif ( $item->{'enclosure'} && $item->{'enclosure'}->{'url'} ) { @@ -751,7 +752,7 @@ sub handleFeed { # make sense to have an All Songs link. (bug 6531) for my $item ( @{ $stash->{'items'} } ) { next unless ( $item->{'type'} && $item->{'type'} eq 'audio' ) || $item->{'enclosure'} || $item->{'play'}; - next unless defined $item->{'duration'}; + next unless defined $item->{'duration'} || $item->{'playall'}; $stash->{'itemsHaveAudio'} = 1; $stash->{'currentIndex'} = $crumb[-1]->{index}; @@ -760,8 +761,11 @@ sub handleFeed { my $itemCount = $feed->{'total'} || scalar @{ $stash->{'items'} }; - my $clientId = ( $client ) ? $client->id : undef; - my $otherParams = '&index=' . $crumb[-1]->{index} . '&player=' . $clientId; + my $clientId = ( $client ) ? $client->id : ''; + my $crumbIndex = $crumb[-1]->{index}; + $crumbIndex = '' if !defined $crumbIndex; + + my $otherParams = '&index=' . $crumbIndex . '&player=' . $clientId; if ( $stash->{'query'} ) { $otherParams = '&query=' . $stash->{'query'} . $otherParams; } @@ -801,7 +805,7 @@ sub handleFeed { } my $item_index = $start; - my $format = $stash->{ajaxSearch} || $stash->{type} eq 'search' + my $format = $stash->{ajaxSearch} || ($stash->{type} || '') eq 'search' ? 'TRACKNUM. TITLE - ALBUM - ARTIST' : $prefs->get('titleFormat')->[ $prefs->get('titleFormatWeb') ]; @@ -822,8 +826,8 @@ sub handleFeed { } # keep track of station icons - if ( - ( $_->{play} || $_->{playlist} || ($_->{type} && ($_->{type} eq 'audio' || $_->{type} eq 'playlist')) ) + if ( $_->{url} && !ref $_->{url} + && ( $_->{play} || $_->{playlist} || ($_->{type} && ($_->{type} eq 'audio' || $_->{type} eq 'playlist')) ) && $_->{url} =~ /^http/ && $_->{url} !~ m|\.com/api/\w+/v1/opml| && ( my $cover = $_->{image} || $_->{cover} ) @@ -937,7 +941,7 @@ sub handleFeed { if ($details->{'unfold'}) { # unfold nested groups of additional items - my $new_index; + my $new_index = 0; foreach my $group (@{ $details->{'unfold'} }) { splice @{ $stash->{'items'} }, ($group->{'start'} + $new_index), 1, @{ $group->{'items'} }; @@ -948,7 +952,7 @@ sub handleFeed { $feed->{'favorites_url'} ||= $stash->{'playUrl'}; - if ($feed->{'hasMetadata'} eq 'album' && $feed->{'albumInfo'}) { + if ($feed->{'hasMetadata'} && $feed->{'hasMetadata'} eq 'album' && $feed->{'albumInfo'}) { my $morelink = _makeWebLink({ actions => $feed->{'albumInfo'} }, $feed, 'info', sprintf('%s (%s)', string('INFORMATION'), ($feed->{'album'} || ''))); @@ -1076,7 +1080,8 @@ sub handleError { my $template = 'xmlbrowser.html'; - my $title = ( uc($params->{title}) eq $params->{title} ) ? Slim::Utils::Strings::getString($params->{title}) : $params->{title}; + $params->{title} ||= ''; + my $title = ( $params->{title} && uc($params->{title}) eq $params->{title} ) ? Slim::Utils::Strings::getString($params->{title}) : $params->{title}; $stash->{'pagetitle'} = $title; $stash->{'pageicon'} = $params->{pageicon}; @@ -1245,7 +1250,7 @@ sub webLink { push @verbs, 'orderBy:' . $args->{'orderBy'} if $args->{'orderBy'}; my $renderCacheKey; - if ( !main::NOBROWSECACHE && $cacheables{ $args->{path} } && $args->{url_query} !~ /\baction=/ && $args->{url_query} !~ /\bindex=\d+\.\d+\.\d+/ && !Slim::Music::Import->stillScanning() ) { + if ( !main::NOBROWSECACHE && $cacheables{ $args->{path} } && !($args->{url_query} && $args->{url_query} =~ /\baction=/) && !($args->{url_query} && $args->{url_query} =~ /\bindex=\d+\.\d+\.\d+/) && !Slim::Music::Import->stillScanning() ) { # let cache expire between server restarts $cacheTimestamp ||= time(); diff --git a/Slim/bootstrap.pm b/Slim/bootstrap.pm index 9a7fac94e04..62ded4d75be 100644 --- a/Slim/bootstrap.pm +++ b/Slim/bootstrap.pm @@ -1,8 +1,6 @@ package Slim::bootstrap; -# $Id$ -# -# Logitech Media Server Copyright 2001-2011 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, version 2 @@ -198,6 +196,7 @@ sub loadModules { print "http://downloads.activestate.com/ActivePerl/releases/\n\n"; } else { + print `perl -v`; print qq{ ******* diff --git a/cleanup.pl b/cleanup.pl index 526e55a73d2..0b07184e11b 100755 --- a/cleanup.pl +++ b/cleanup.pl @@ -1,6 +1,6 @@ -#!/usr/bin/perl -ICPAN +#!/usr/bin/env perl -ICPAN -# Logitech Media Server Copyright 2001-2009 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -21,9 +21,9 @@ my $splash; my $useWx = (!ISMAC || $^X =~ /wxPerl/i) && eval { require Wx; - + showSplashScreen(); - + require Wx::Event; require Slim::GUI::ControlPanel; @@ -58,7 +58,7 @@ require Slim::Utils::OSDetect; require Slim::Utils::Light; -our $VERSION = '7.9.0'; +our $VERSION = '7.9.4'; BEGIN { if (ISWINDOWS) { @@ -76,14 +76,14 @@ BEGIN sub main { Slim::Utils::OSDetect::init(); $os = Slim::Utils::OSDetect->getOS(); - + if (checkForSC() && !$useWx) { print sprintf("\n%s\n\n", Slim::Utils::Light::string('CLEANUP_PLEASE_STOP_SC')); exit; } my ($all, $cache, $filecache, $database, $prefs, $logs, $dryrun); - + Getopt::Long::GetOptions( 'all' => \$all, 'cache' => \$cache, @@ -93,7 +93,7 @@ sub main { 'database' => \$database, 'dryrun' => \$dryrun, ); - + my $folders = getFolderList({ 'all' => $all, 'cache' => $cache, @@ -102,32 +102,32 @@ sub main { 'logs' => $logs, 'database' => $database, }); - + unless (scalar @$folders) { # show simple GUI if possible if ($useWx) { - + my $app = Slim::GUI::ControlPanel->new({ folderCB => \&getFolderList, cleanCB => \&cleanup, options => options(), }); - + $splash->Destroy(); - + $app->MainLoop; exit; } - else { + else { usage(); exit; } } cleanup($folders, $dryrun); - + print sprintf("\n%s\n\n", Slim::Utils::Light::string('CLEANUP_PLEASE_RESTART_SC')); } @@ -145,12 +145,12 @@ sub usage { --cache (!) %s --all (!!) %s - + --dryrun %s - + EOF - print sprintf($usage, - Slim::Utils::Light::string('CLEANUP_USAGE'), + print sprintf($usage, + Slim::Utils::Light::string('CLEANUP_USAGE'), Slim::Utils::Light::string('CLEANUP_COMMAND_LINE'), Slim::Utils::Light::string('CLEANUP_DB'), Slim::Utils::Light::string('CLEANUP_FILECACHE'), @@ -164,12 +164,12 @@ sub usage { sub getFolderList { my $args = shift; - + my @folders; my $cacheFolder = Slim::Utils::Light::getPref('cachedir') || $os->dirsFor('cache'); push @folders, _target('cache', 'cache') if ($args->{all} || $args->{cache}); - + if ($args->{all} || $args->{prefs} || $args->{cache} || $args->{filecache} || $args->{logs} || $args->{database}) { push @folders, { label => 'some legacy files', @@ -191,17 +191,19 @@ sub getFolderList { ], }; } - + if ($args->{filecache}) { push @folders, { label => 'file cache (artwork, templates etc.)', folders => [ File::Spec::Functions::catdir($cacheFolder, 'Artwork'), File::Spec::Functions::catdir($cacheFolder, 'ArtworkCache'), + File::Spec::Functions::catdir($cacheFolder, 'audioUploads'), File::Spec::Functions::catdir($cacheFolder, 'iTunesArtwork'), File::Spec::Functions::catdir($cacheFolder, 'FileCache'), File::Spec::Functions::catdir($cacheFolder, 'fonts.bin'), File::Spec::Functions::catdir($cacheFolder, 'strings.bin'), + File::Spec::Functions::catdir($cacheFolder, 'extrastrings.json'), File::Spec::Functions::catdir($cacheFolder, 'templates'), File::Spec::Functions::catdir($cacheFolder, 'updates'), File::Spec::Functions::catdir($cacheFolder, 'cookies.dat'), @@ -214,10 +216,14 @@ sub getFolderList { File::Spec::Functions::catdir($cacheFolder, 'artwork.db'), File::Spec::Functions::catdir($cacheFolder, 'artwork.db-shm'), File::Spec::Functions::catdir($cacheFolder, 'artwork.db-wal'), + + File::Spec::Functions::catdir($cacheFolder, 'imgproxy.db'), + File::Spec::Functions::catdir($cacheFolder, 'imgproxy.db-shm'), + File::Spec::Functions::catdir($cacheFolder, 'imgproxy.db-wal'), ], }; } - + if ($args->{database}) { push @folders, { label => 'Musiclibrary data', @@ -235,12 +241,12 @@ sub getFolderList { ], }; } - + if ($args->{all} || $args->{prefs}) { push @folders, _target('prefs', 'preferences'); push @folders, _target('oldprefs', 'old preferences (SlimServer <= 6.5)'); } - + push @folders, _target('log', 'logs') if ($args->{all} || $args->{logs}); return \@folders; @@ -248,9 +254,9 @@ sub getFolderList { sub _target { my ($value, $label) = @_; - + my $f = $os->dirsFor($value); - + return { label => $label, folders => [ $f ], @@ -258,43 +264,43 @@ sub _target { } sub options { - + my $options = [ { name => 'prefs', title => Slim::Utils::Light::string('CLEANUP_PREFS'), position => [30, 20], }, - + { name => 'filecache', title => Slim::Utils::Light::string('CLEANUP_FILECACHE'), position => [30, 40], }, - + { - + name => 'database', title => Slim::Utils::Light::string('CLEANUP_DB'), position => [30, 60], }, - + { - + name => 'logs', title => Slim::Utils::Light::string('CLEANUP_LOGS'), position => [30, 80], }, - + { - + name => 'cache', title => Slim::Utils::Light::string('CLEANUP_CACHE'), position => [30, 120], }, - + { - + name => 'all', title => '(!) ' . Slim::Utils::Light::string('CLEANUP_ALL'), position => [30, 160], @@ -324,18 +330,18 @@ sub cleanup { for my $item (@$folders) { print sprintf("\n%s %s...\n", Slim::Utils::Light::string('CLEANUP_DELETING'), $item->{label}) unless $useWx; - + foreach ( @{$item->{folders}} ) { next unless $_; - + print "-> $_\n" if (-e $_ && !$useWx); - + next if $dryrun; if (-d $_) { File::Path::rmtree($_); } - + elsif (-f $_) { unlink $_; } @@ -345,23 +351,23 @@ sub cleanup { sub showSplashScreen { return unless $^O =~ /win/i; - + my $file; - + if (defined $PerlApp::VERSION) { $file = PerlApp::extract_bound_file(SPLASH_LOGO); } - + if (!$file || !-f $file) { $file = '../platforms/win32/res/' . SPLASH_LOGO; } Wx::Image::AddHandler(Wx::PNGHandler->new()); - + if (my $bitmap = Wx::Bitmap->new($file, Wx::wxBITMAP_TYPE_PNG())) { $splash = Wx::SplashScreen->new( - $bitmap, + $bitmap, Wx::wxSPLASH_CENTRE_ON_SCREEN() | Wx::wxSPLASH_NO_TIMEOUT(), 0, undef, diff --git a/convert.conf b/convert.conf index 2631b09772f..f4b901b6cd7 100644 --- a/convert.conf +++ b/convert.conf @@ -1,5 +1,3 @@ -# $Id$ -# # Configuration file for transcoding # # If you wish to create custom transcoding entries that won't be overwritten @@ -89,6 +87,10 @@ mp4 mp3 * * # FB:{BITRATE=--abr %B}T:{START=-j %s}U:{END=-e %u} [faad] -q -w -f 1 $START$ $END$ $FILE$ | [lame] --silent -q $QUALITY$ $BITRATE$ - - +mp4x mp3 * * + # FB:{BITRATE=--abr %B}T:{START=-j %s}U:{END=-e %u} + [faad] -q -w -f 1 $START$ $END$ $FILE$ | [lame] --silent -q $QUALITY$ $BITRATE$ - - + aac mp3 * * # IFB:{BITRATE=--abr %B} [faad] -q -w -f 1 $FILE$ | [lame] --silent -q $QUALITY$ $BITRATE$ - - @@ -101,6 +103,10 @@ alc mp3 * * # FB:{BITRATE=--abr %B}D:{RESAMPLE=--resample %D}T:{START=-j %s}U:{END=-e %u} [faad] -q -w -f 1 $START$ $END$ $FILE$ | [lame] --silent -q $QUALITY$ $RESAMPLE$ $BITRATE$ - - +alcx mp3 * * + # FB:{BITRATE=--abr %B}D:{RESAMPLE=--resample %D}T:{START=-j %s}U:{END=-e %u} + [faad] -q -w -f 1 $START$ $END$ $FILE$ | [lame] --silent -q $QUALITY$ $RESAMPLE$ $BITRATE$ - - + ogg mp3 * * # IFB:{BITRATE=--abr %B}D:{RESAMPLE=--resample %D} [sox] -q -t ogg $FILE$ -t wav - | [lame] --silent -q $QUALITY$ $RESAMPLE$ $BITRATE$ - - @@ -173,6 +179,23 @@ ogg aif * * ogg pcm * * [sox] -q -t ogg $FILE$ -t raw -r 44100 -c 2 -2 -s - + +ops ops * * + - + +ops mp3 * * + # IFB:{BITRATE=--abr %B}D:{RESAMPLE=--resample %D} + [sox] -q -t opus $FILE$ -t wav - | [lame] --silent -q $QUALITY$ $RESAMPLE$ $BITRATE$ - - + +ops aif * * + [sox] -q -t opus $FILE$ -t raw -r 44100 -c 2 -2 -s $-x$ - + +ops pcm * * + [sox] -q -t opus $FILE$ -t raw -r 44100 -c 2 -2 -s - + +ops flc * * + # IFRD:{RESAMPLE=-r %d}T:{START=trim %s} + [sox] -t opus $FILE$ -t flac -C 0 $RESAMPLE$ - $START$ wma pcm * * # F:{PATH=%f}R:{PATH=%F} @@ -201,6 +224,10 @@ mpc aif * * alc pcm * * # FT:{START=-j %s}U:{END=-e %u} [faad] -q -w -f 2 $START$ $END$ $FILE$ + +alcx pcm * * + # FT:{START=-j %s}U:{END=-e %u} + [faad] -q -w -f 2 $START$ $END$ $FILE$ wvp pcm * * # FT:{START=--skip=%t}U:{END=--until=%v} @@ -210,6 +237,10 @@ mp4 pcm * * # FT:{START=-j %s}U:{END=-e %u} [faad] -q -w -f 2 -b 1 $START$ $END$ $FILE$ +mp4x pcm * * + # FT:{START=-j %s}U:{END=-e %u} + [faad] -q -w -f 2 -b 1 $START$ $END$ $FILE$ + aac pcm * * # IF [faad] -q -w -f 2 -b 1 $FILE$ @@ -271,6 +302,10 @@ mp4 flc * * # FT:{START=-j %s}U:{END=-e %u} [faad] -q -w -f 1 $START$ $END$ $FILE$ | [flac] -cs --totally-silent --compression-level-0 --ignore-chunk-sizes - +mp4x flc * * + # FT:{START=-j %s}U:{END=-e %u} + [faad] -q -w -f 1 $START$ $END$ $FILE$ | [flac] -cs --totally-silent --compression-level-0 --ignore-chunk-sizes - + aac flc * * # IF [faad] -q -w -f 1 $FILE$ | [flac] -cs --totally-silent --compression-level-0 --ignore-chunk-sizes - @@ -283,6 +318,11 @@ alc flc * * # FT:{START=-j %s}U:{END=-e %u}D:{RESAMPLE=-r %d} [faad] -q -w -f 1 $START$ $END$ $FILE$ | [sox] -q -t wav - -t flac -C 0 $RESAMPLE$ - +alcx flc * * + # FT:{START=-j %s}U:{END=-e %u}D:{RESAMPLE=-r %d} + [faad] -q -w -f 1 $START$ $END$ $FILE$ | [sox] -q -t wav - -t flac -C 0 $RESAMPLE$ - + + wvp flc * * # FT:{START=--skip=%t}U:{END=--until=%v}D:{RESAMPLE=-r %d} [wvunpack] $FILE$ -wq $START$ $END$ -o - | [sox] -q -t wav - -t flac -C 0 $RESAMPLE$ - diff --git a/gdresized.pl b/gdresized.pl index 6b0418ceee7..f767cdf5aa8 100755 --- a/gdresized.pl +++ b/gdresized.pl @@ -27,12 +27,12 @@ BEGIN use Config; use File::Spec::Functions qw(catdir); use Slim::Utils::OSDetect; # XXX would be nice to do without this - + my $libPath = $Bin; my @SlimINC = (); - + Slim::Utils::OSDetect::init(); - + if (my $libs = Slim::Utils::OSDetect::dirsFor('libpath')) { # On Debian, RH and SUSE, our CPAN directory is located in the same dir as strings.txt $libPath = $libs; @@ -44,7 +44,7 @@ BEGIN my $arch = $Config::Config{'archname'}; $arch =~ s/^i[3456]86-/i386-/; $arch =~ s/gnu-//; - + # Check for use64bitint Perls my $is64bitint = $arch =~ /64int/; @@ -52,12 +52,12 @@ BEGIN # can run our binaries, this will fail for some people running invalid versions of Perl # but that's OK, they'd be broken anyway. if ( $arch =~ /^arm.*linux/ ) { - $arch = $arch =~ /gnueabihf/ - ? 'arm-linux-gnueabihf-thread-multi' + $arch = $arch =~ /gnueabihf/ + ? 'arm-linux-gnueabihf-thread-multi' : 'arm-linux-gnueabi-thread-multi'; $arch .= '-64int' if $is64bitint; } - + # Same thing with PPC if ( $arch =~ /^(?:ppc|powerpc).*linux/ ) { $arch = 'powerpc-linux-thread-multi'; @@ -76,8 +76,8 @@ BEGIN catdir($libPath,'CPAN','arch',$perlmajorversion, $Config::Config{'archname'}, 'auto'), catdir($libPath,'CPAN','arch',$Config::Config{'archname'}), catdir($libPath,'CPAN','arch',$perlmajorversion), - catdir($libPath,'lib'), - catdir($libPath,'CPAN'), + catdir($libPath,'lib'), + catdir($libPath,'CPAN'), $libPath, ); @@ -120,59 +120,82 @@ BEGIN exit 0; }; -my $cache = Slim::Utils::ArtworkCache->new('.'); +my $artworkCache = Slim::Utils::ArtworkCache->new('.'); +my $imageProxyCache = Slim::Web::ImageProxy::Cache->new('.'); DEBUG && warn "$0 listening on " . SOCKET_PATH . "\n"; while (1) { my $client = $socket->accept(); - + eval { DEBUG && (my $tv = Time::HiRes::time()); - + # get command my $buf = <$client>; - - my ($file, $spec, $cacheroot, $cachekey) = unpack 'Z*Z*Z*Z*', $buf; - + + my ($file, $spec, $cacheroot, $cachekey, $data) = unpack 'Z* Z* Z* Z* Z*', $buf; + + if ($data) { + require MIME::Base64; + $data = MIME::Base64::decode_base64($data); + } + # An empty spec is allowed, this returns the original image $spec ||= 'XxX'; - - DEBUG && warn "file=$file, spec=$spec, cacheroot=$cacheroot, cachekey=$cachekey\n"; - + + my $imageproxy = $cachekey =~ /^imageproxy/; + DEBUG && warn sprintf("file=%s, spec=%s, cacheroot=%s, cachekey=%s, imageproxy=%s, imagedata=%s bytes\n", $file, $spec, $cacheroot, $cachekey, $imageproxy || 0, length($data)); + if ( !$file || !$spec || !$cacheroot || !$cachekey ) { - die "Invalid parameters: $file, $spec, $cacheroot, $cachekey\n"; + die sprintf("Invalid parameters: file=%s, spec=%s, cacheroot=%s, cachekey=%s, imageproxy=%s, imagedata=%s bytes\n", $file, $spec, $cacheroot, $cachekey, $imageproxy || 0, length($data || '')); } - + my @spec = split ',', $spec; - + + my $cache = $imageproxy ? $imageProxyCache : $artworkCache; if ( $cache->getRoot() ne $cacheroot ) { $cache->setRoot($cacheroot); $cache->pragma('locking_mode = NORMAL'); } - + # do resize Slim::Utils::GDResizer->gdresize( - file => $file, + file => $data ? \$data : $file, debug => DEBUG, faster => $faster, cache => $cache, cachekey => $cachekey, spec => \@spec, ); - + # send result print $client "OK\015\012"; - + DEBUG && warn "OK (" . (Time::HiRes::time() - $tv) . " seconds)\n"; }; - + if ( $@ ) { print $client "Error: $@\015\012"; warn "$@\n"; } } +1; + +package Slim::Web::ImageProxy::Cache; + +use base 'Slim::Utils::DbArtworkCache'; + +use strict; + +sub new { + my $class = shift; + my $root = shift; + + return $class->SUPER::new($root, 'imgproxy', 86400*30); +} + __END__ =head1 NAME diff --git a/icudt58b.dat b/icudt58b.dat new file mode 100644 index 00000000000..8b98a9109e0 Binary files /dev/null and b/icudt58b.dat differ diff --git a/icudt58l.dat b/icudt58l.dat new file mode 100644 index 00000000000..c2fedb8a543 Binary files /dev/null and b/icudt58l.dat differ diff --git a/lib/AnyEvent/DNS.pm b/lib/AnyEvent/DNS.pm index f7afc5b7a97..8f72fafddba 100644 --- a/lib/AnyEvent/DNS.pm +++ b/lib/AnyEvent/DNS.pm @@ -1339,7 +1339,7 @@ sub resolve($%) { if @rr; # see if there is a cname we can follow - my @rr = grep $name eq lc $_->[0] && $_->[1] eq "cname", @{ $res->{an} }; + @rr = grep $name eq lc $_->[0] && $_->[1] eq "cname", @{ $res->{an} }; if (@rr) { $depth-- diff --git a/CPAN/AnyEvent/Handle.pm b/lib/AnyEvent/Handle.pm similarity index 99% rename from CPAN/AnyEvent/Handle.pm rename to lib/AnyEvent/Handle.pm index 58cc24ca1e5..4af8409743d 100644 --- a/CPAN/AnyEvent/Handle.pm +++ b/lib/AnyEvent/Handle.pm @@ -1770,7 +1770,7 @@ sub _tls_error { return $self->_error ($!, 1) if $err == Net::SSLeay::ERROR_SYSCALL (); - my $err =Net::SSLeay::ERR_error_string (Net::SSLeay::ERR_get_error ()); + $err =Net::SSLeay::ERR_error_string (Net::SSLeay::ERR_get_error ()); # reduce error string to look less scary $err =~ s/^error:[0-9a-fA-F]{8}:[^:]+:([^:]+):/\L$1: /; diff --git a/CPAN/AnyEvent/Socket.pm b/lib/AnyEvent/Socket.pm similarity index 99% rename from CPAN/AnyEvent/Socket.pm rename to lib/AnyEvent/Socket.pm index 17f372fe5bf..1b74e855d52 100644 --- a/CPAN/AnyEvent/Socket.pm +++ b/lib/AnyEvent/Socket.pm @@ -459,10 +459,11 @@ Example. sub inet_aton { my ($name, $cb) = @_; + my $ipn; - if (my $ipn = &parse_ipv4) { + if ($ipn = &parse_ipv4) { $cb->($ipn); - } elsif (my $ipn = &parse_ipv6) { + } elsif ($ipn = &parse_ipv6) { $cb->($ipn); } elsif ($name eq "localhost") { # rfc2606 et al. $cb->(v127.0.0.1, v0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1); diff --git a/lib/Audio/Scan.pm b/lib/Audio/Scan.pm index 4584fdafe9f..5741bfd6286 100644 --- a/lib/Audio/Scan.pm +++ b/lib/Audio/Scan.pm @@ -7,7 +7,7 @@ our $VERSION; require XSLoader; BEGIN { - foreach ('0.93', '0.95', '0.94') { + foreach ('0.99', '0.93', '0.95', '0.94') { eval { XSLoader::load('Audio::Scan', $_); }; if (!$@) { diff --git a/modules.conf b/modules.conf index 6a2c5f6dbfe..96bc823d7c7 100644 --- a/modules.conf +++ b/modules.conf @@ -5,7 +5,6 @@ # [ ] AnyEvent 5.202 -Audio::Scan 0.93 0.95 CGI::Cookie 1.27 Class::Data::Inheritable 0.04 Class::Inspector 1.16 diff --git a/scanner.pl b/scanner.pl index de65a803c55..b98ea2b5975 100755 --- a/scanner.pl +++ b/scanner.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Logitech Media Server Copyright 2001-2009 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -92,7 +92,7 @@ BEGIN require Slim::Utils::PerlRunTime; } -our $VERSION = '7.9.0'; +our $VERSION = '7.9.4'; our $REVISION = undef; our $BUILDDATE = undef; diff --git a/slimserver.pl b/slimserver.pl index 670ceda7ef8..07af6803c18 100755 --- a/slimserver.pl +++ b/slimserver.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Logitech Media Server Copyright 2001-2009 Logitech. +# Logitech Media Server Copyright 2001-2020 Logitech. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. @@ -27,7 +27,7 @@ BEGIN Delimiter =>'/' } ); - + if ($swKey) { push @ARGV, split(" ", $swKey->{ImagePath}); shift @ARGV; # remove script name @@ -63,83 +63,86 @@ BEGIN # This package section is used for the windows service version of the application, # as built with ActiveState's PerlSvc -package PerlSvc; +if (ISWINDOWS && $PerlSvc::VERSION) { + package PerlSvc; + + our %Config = ( + DisplayName => 'Logitech Media Server', + Description => "Logitech Media Server - streaming media server", + ServiceName => "squeezesvc", + StartNow => 0, + ); -our %Config = ( - DisplayName => 'Logitech Media Server', - Description => "Logitech Media Server - streaming media server", - ServiceName => "squeezesvc", - StartNow => 0, -); + sub Startup { + # Tell PerlSvc to bundle these modules + if (0) { + require 'auto/Compress/Raw/Zlib/autosplit.ix'; + require Cache::FileCache; + } + + # added to workaround a problem with 5.8 and perlsvc. + # $SIG{BREAK} = sub {} if RunningAsService(); + main::initOptions(); + main::init(); -sub Startup { - # Tell PerlSvc to bundle these modules - if (0) { - require 'auto/Compress/Raw/Zlib/autosplit.ix'; - require Cache::FileCache; + # here's where your startup code will go + while (ContinueRun() && !main::idle()) { } + + main::stopServer(); } - - # added to workaround a problem with 5.8 and perlsvc. - # $SIG{BREAK} = sub {} if RunningAsService(); - main::initOptions(); - main::init(); - - # here's where your startup code will go - while (ContinueRun() && !main::idle()) { } - - main::stopServer(); -} -sub Install { + sub Install { - my($Username,$Password); + my($Username,$Password); - use Getopt::Long; + use Getopt::Long; - Getopt::Long::GetOptions( - 'username=s' => \$Username, - 'password=s' => \$Password, - ); + Getopt::Long::GetOptions( + 'username=s' => \$Username, + 'password=s' => \$Password, + ); - main::initLogging(); + main::initLogging(); - if ((defined $Username) && ((defined $Password) && length($Password) != 0)) { - my @infos; - my ($host, $user); + if ((defined $Username) && ((defined $Password) && length($Password) != 0)) { + my @infos; + my ($host, $user); - # use the localhost '.' by default, unless user has defined "domain\username" - if ($Username =~ /(.+)\\(.+)/) { - $host = $1; - $user = $2; - } - else { - $host = '.'; - $user = $Username; - } + # use the localhost '.' by default, unless user has defined "domain\username" + if ($Username =~ /(.+)\\(.+)/) { + $host = $1; + $user = $2; + } + else { + $host = '.'; + $user = $Username; + } - # configure user to be used to run the server - my $grant = PerlSvc::extract_bound_file('grant.exe'); - if ($host && $user && $grant && !`$grant add SeServiceLogonRight $user`) { - $Config{UserName} = "$host\\$user"; - $Config{Password} = $Password; + # configure user to be used to run the server + my $grant = PerlSvc::extract_bound_file('grant.exe'); + if ($host && $user && $grant && !`$grant add SeServiceLogonRight $user`) { + $Config{UserName} = "$host\\$user"; + $Config{Password} = $Password; + } } } -} -sub Interactive { - main::main(); -} + sub Interactive { + main::main(); + } -sub Remove { - # add your additional remove messages or functions here - main::initLogging(); -} + sub Remove { + # add your additional remove messages or functions here + main::initLogging(); + } -sub Help { - main::showUsage(); - main::initLogging(); + sub Help { + main::showUsage(); + main::initLogging(); + } } + package main; use FindBin qw($Bin); @@ -155,14 +158,14 @@ BEGIN # set the AnyEvent model to our subclassed version when PERFMON is enabled $ENV{PERL_ANYEVENT_MODEL} = 'PerfMonEV' if main::PERFMON; $ENV{PERL_ANYEVENT_MODEL} ||= 'EV'; - + # By default, tell Audio::Scan not to get artwork to save memory # Where needed, this is locally changed to 0. $ENV{AUDIO_SCAN_NO_ARTWORK} = 1; # save argv @argv = @ARGV; - + use Slim::bootstrap; use Slim::Utils::OSDetect; @@ -181,15 +184,15 @@ BEGIN my $HAS_AIO; sub HAS_AIO { return $HAS_AIO if defined $HAS_AIO; - + eval { require AnyEvent::AIO; IO::AIO::max_poll_time( 0.01 ); # Make AIO play nice if there are a lot of requests (10ms per poll) $HAS_AIO = 1; }; - + $HAS_AIO = 0 if !$HAS_AIO; # Make sure it is defined now. - + return $HAS_AIO; } @@ -200,7 +203,7 @@ sub MEDIASUPPORT { eval { $MEDIASUPPORT = (main::IMAGE || main::VIDEO) && (Slim::Utils::PluginManager->isEnabled('Slim::Plugin::UPnP::Plugin') ? 1 : 0); }; - + return $MEDIASUPPORT; } @@ -208,7 +211,7 @@ sub MEDIASUPPORT { # Force XML::Simple to use XML::Parser for speed. This is done # here so other packages don't have to worry about it. If we # don't have XML::Parser installed, we fall back to PurePerl. -# +# # Only use XML::Simple 2.15 an above, which has support for pass-by-ref use XML::Simple qw(2.15); @@ -316,7 +319,7 @@ sub MEDIASUPPORT { my $prefs = preferences('server'); -our $VERSION = '7.9.0'; +our $VERSION = '7.9.4'; our $REVISION = undef; our $BUILDDATE = undef; our $httpport = undef; @@ -360,10 +363,10 @@ sub MEDIASUPPORT { sub init { $inInit = 1; - + # May get overridden by object-leak or nytprof usage below $SIG{USR2} = \&Slim::Utils::Log::logBacktrace; - + # Can only have one of NYTPROF and Object-Leak at a time if ( $ENV{OBJECT_LEAK} ) { require Devel::Leak::Object; @@ -373,7 +376,7 @@ sub init { warn "Dumping objects...\n"; }; } - + # initialize the process and daemonize, etc... srand(); @@ -389,12 +392,12 @@ sub init { # force a charset from the command line $Slim::Utils::Unicode::lc_ctype = $charset if $charset; - + # If dbsource has been changed via settings, it overrides the default if ( $prefs->get('dbtype') ) { $dbtype ||= $prefs->get('dbtype') =~ /SQLite/ ? 'SQLite' : 'MySQL'; } - + if ( $dbtype ) { # For testing SQLite, can specify a different database type $sqlHelperClass = "Slim::Utils::${dbtype}Helper"; @@ -424,10 +427,10 @@ sub init { } $SIG{__WARN__} = sub { msg($_[0]) }; - + # Uncomment to enable crash debugging. #$SIG{__DIE__} = \&Slim::Utils::Misc::bt; - + # Start/stop profiler during runtime (requires Devel::NYTProf) # and NYTPROF env var set to 'start=no' if ( $ENV{NYTPROF} && $INC{'Devel/NYTProf.pm'} && $ENV{NYTPROF} =~ /start=no/ ) { @@ -435,7 +438,7 @@ sub init { DB::enable_profile(); warn "Profiling enabled...\n"; }; - + $SIG{USR2} = sub { DB::finish_profile(); warn "Profiling disabled...\n"; @@ -452,7 +455,7 @@ sub init { save_pid_file(); } - + # leave a mark for external tools $failsafe ? $prefs->set('failsafe', 1) : $prefs->remove('failsafe'); @@ -468,7 +471,7 @@ sub init { } else { Slim::Utils::Misc::setPriority( $prefs->get('serverPriority') ); } - + # Generate a UUID for this SC instance on first-run if ( !$prefs->get('server_uuid') ) { require UUID::Tiny; @@ -484,7 +487,7 @@ sub init { main::INFOLOG && $log->info("Server strings init..."); Slim::Utils::Strings::init(); - + # Load appropriate DB module my $dbModule = $sqlHelperClass =~ /MySQL/ ? 'DBD::mysql' : 'DBD::SQLite'; Slim::bootstrap::tryModuleLoad($dbModule); @@ -497,23 +500,23 @@ sub init { main::INFOLOG && $log->info("Server SQL init ($sqlHelperClass)..."); $sqlHelperClass->init(); } - + main::INFOLOG && $log->info("Async DNS init..."); Slim::Networking::Async::DNS->init; - + main::INFOLOG && $log->info("Async HTTP init..."); Slim::Networking::Async::HTTP->init; Slim::Networking::SimpleAsyncHTTP->init; - + if (!main::NOMYSB) { main::INFOLOG && $log->info("SqueezeNetwork Init..."); require Slim::Networking::SqueezeNetwork; Slim::Networking::SqueezeNetwork->init(); } - + main::INFOLOG && $log->info("Download repositories init..."); Slim::Networking::Repositories->init(); - + main::INFOLOG && $log->info("Firmware init..."); Slim::Utils::Firmware->init; @@ -525,10 +528,10 @@ sub init { main::INFOLOG && $log->info("Server Request init..."); Slim::Control::Request::init(); - + main::INFOLOG && $log->info("Server Queries init..."); Slim::Control::Queries->init(); - + main::INFOLOG && $log->info("Server Buttons init..."); Slim::Buttons::Common::init(); @@ -545,12 +548,12 @@ sub init { main::INFOLOG && $log->info("Cache init..."); Slim::Utils::Cache->init(); - + Slim::Schema->init(); Slim::Schema::RemoteTrack->init(); - + Slim::Music::VirtualLibraries->init(); - + # Register the default importers - necessary to ensure that Slim::Schema::init() is called # but no need to initialize it, as it's being run in external scanner mode only # XXX - we should be able to handle this differently @@ -574,7 +577,7 @@ sub init { require Slim::Web::Setup; Slim::Web::Setup::initSetup(); } - + main::INFOLOG && $log->info('Menu init...'); Slim::Menu::TrackInfo->init(); Slim::Menu::AlbumInfo->init(); @@ -595,7 +598,7 @@ sub init { main::INFOLOG && $log->info("Server Jive init..."); Slim::Control::Jive->init(); - + # Reinitialize logging, as plugins may have been added. if (Slim::Utils::Log->needsReInit) { @@ -604,17 +607,17 @@ sub init { main::INFOLOG && $log->info("Server checkDataSource..."); checkDataSource(); - + if ( $os->canAutoRescan && $prefs->get('autorescan') ) { require Slim::Utils::AutoRescan; - + main::INFOLOG && $log->info('Auto-rescan init...'); Slim::Utils::AutoRescan->init(); } main::INFOLOG && $log->info("Library Browser init..."); Slim::Menu::BrowseLibrary->init(); - + # regular server has a couple more initial operations. main::INFOLOG && $log->info("Server persist playlists..."); @@ -657,14 +660,14 @@ sub init { # otherwise, get ready to loop $lastlooptime = Time::HiRes::time(); - + $inInit = 0; main::INFOLOG && $log->info("Server done init..."); } sub main { - + # command line options initOptions(); @@ -693,28 +696,28 @@ sub idle { } $lastlooptime = $now; - + # This flag indicates we have pending IR or request events to handle my $pendingEvents = 0; - + # process IR queue $pendingEvents = Slim::Hardware::IR::idle(); - + if ( !$pendingEvents ) { # empty notifcation queue, only if no IR events are pending $pendingEvents = Slim::Control::Request::checkNotifications(); - + if ( !$pendingEvents ) { # run scheduled tasks, only if no other events are pending $pendingEvents = Slim::Utils::Scheduler::run_tasks(); } } - + # Include pending AIO events or we will end up stalling AIO processing if ( $HAS_AIO && !$pendingEvents ) { $pendingEvents += IO::AIO::nreqs(); } - + if ( $pendingEvents ) { # Some notifications are still pending, run the loop in non-blocking mode Slim::Networking::IO::Select::loop( EV::LOOP_NONBLOCK ); @@ -729,10 +732,10 @@ sub idle { sub idleStreams { my $timeout = shift || 0; - + # No idle processing during startup return if $inInit; - + # Loop once without blocking Slim::Networking::IO::Select::loop( EV::LOOP_NONBLOCK ); } @@ -910,7 +913,7 @@ sub initSettings { $prefs->set('cachedir', $cachedir); $prefs->set('librarycachedir', $cachedir); } - + if (defined($httpport)) { $prefs->set('httpport', $httpport); } @@ -918,17 +921,17 @@ sub initSettings { if (defined($cliport)) { preferences('plugin.cli')->set('cliport', $cliport); } - + if (defined($prefs->get('cachedir')) && $prefs->get('cachedir') ne '') { $cachedir = $prefs->get('cachedir'); $cachedir = Slim::Utils::Misc::fixPath($cachedir); $cachedir = Slim::Utils::Misc::pathFromFileURL($cachedir); $cachedir =~ s|[/\\]$||; - + # Make sure cachedir exists Slim::Utils::Prefs::makeCacheDir($cachedir); - + $prefs->set('cachedir',$cachedir); $prefs->set('librarycachedir',$cachedir) unless $prefs->get('librarycachedir'); } @@ -1020,7 +1023,7 @@ sub changeEffectiveUserAndGroup { endgrent(); - # If a group was specified, get the gid of it and add it to the + # If a group was specified, get the gid of it and add it to the # list of supplementary groups. if (defined($group)) { $gid = getgrnam($group); @@ -1045,11 +1048,11 @@ sub changeEffectiveUserAndGroup { # any supplementary group IDs, so compare against that. On some systems # no supplementary group IDs are present at system startup or at all. - # We need to pass $pgid twice because setgroups only gets called if there's + # We need to pass $pgid twice because setgroups only gets called if there's # more than one value. For example, if we did: # $) = "1234" - # then the effective primary group would become 1234, but we'd retain any - # previously set supplementary groups. To become a member of just 1234, the + # then the effective primary group would become 1234, but we'd retain any + # previously set supplementary groups. To become a member of just 1234, the # correct way is to do: # $) = "1234 1234" @@ -1087,12 +1090,9 @@ sub checkDataSource { $prefs->set('mediadirs', $mediadirs) if $modified; return if !Slim::Schema::hasLibrary(); - + $sqlHelperClass->checkDataSource(); - - # Don't launch an initial scan on SqueezeOS, it will be handled by AutoRescan - return if Slim::Utils::OSDetect::isSqueezeOS(); - + # Count entries for all media types, run scan if all are empty my $dbh = Slim::Schema->dbh; my ($gc, $vc, $ic) = $dbh->selectrow_array( qq{ @@ -1130,7 +1130,7 @@ sub restartServer { if ( canRestartServer() ) { cleanup(); logger('')->info( 'Logitech Media Server restarting...' ); - + if ( !Slim::Utils::OSDetect->getOS()->restartServer($0, \@argv) ) { logger('')->error("Unable to restart Logitech Media Server"); } @@ -1150,13 +1150,13 @@ sub stopServer { cleanup(); logger('')->info( 'Logitech Media Server shutting down.' ); - + exit(); } sub cleanup { logger('')->info("Logitech Media Server cleaning up."); - + $::stop = 1; # Make sure to flush anything in the database to disk. @@ -1194,7 +1194,7 @@ sub save_pid_file { File::Slurp::write_file($pidfile, $process_id); } } - + sub remove_pid_file { if (defined $pidfile) { unlink $pidfile; @@ -1205,8 +1205,8 @@ sub END { Slim::bootstrap::theEND(); } - -# start up the server if we're not running as a service. + +# start up the server if we're not running as a service. if (!defined($PerlSvc::VERSION)) { main() } diff --git a/strings.txt b/strings.txt index d9aa1702150..d0e038af941 100644 --- a/strings.txt +++ b/strings.txt @@ -90,49 +90,49 @@ CREDITS ZH_CN Kok-Bin Lee COPYRIGHT - CS © 2001-2011 Logitech Verze 7.9.0 - DA © 2001-2011 Logitech - version 7.9.0 - DE © 2001-2011 Logitech Version 7.9.0 - EN © 2001-2011 Logitech Version 7.9.0 - ES © 2001-2011 Logitech Versión 7.9.0 - FI © 2001-2011 Logitechin versio 7.9.0 - FR © 2001-2011 Logitech Version 7.9.0 - IT © 2001-2011 Logitech versione 7.9.0 - NL © 2001-2011 Logitech versie 7.9.0 - NO © 2001–2011 Logitech versjon 7.9.0 - PL © 2001-2011 Logitech wersja 7.9.0 - RU © Logitech, 2001-2011. Версия 7.9.0 - SV © 2001–2011 Logitech Version 7.9.0 + CS © 2001-2020 Logitech Verze 7.9.4 + DA © 2001-2020 Logitech - version 7.9.4 + DE © 2001-2020 Logitech Version 7.9.4 + EN © 2001-2020 Logitech Version 7.9.4 + ES © 2001-2020 Logitech Versión 7.9.4 + FI © 2001-2020 Logitechin versio 7.9.4 + FR © 2001-2020 Logitech Version 7.9.4 + IT © 2001-2020 Logitech versione 7.9.4 + NL © 2001-2020 Logitech versie 7.9.4 + NO © 2001–2011 Logitech versjon 7.9.4 + PL © 2001-2020 Logitech wersja 7.9.4 + RU © Logitech, 2001-2020. Версия 7.9.4 + SV © 2001–2011 Logitech Version 7.9.4 COPYRIGHT_LOGITECH - CS © 2001-2012 Logitech - DA © 2001-2012 Logitech - DE © 2001-2012 Logitech - EN © 2001-2012 Logitech - ES © 2001-2012 Logitech - FI © 2001-2012 Logitech - FR © 2001-2012 Logitech - IT © 2001-2012 Logitech - NL © 2001-2012 Logitech - NO © 2001-2012 Logitech - PL © 2001-2012 Logitech - RU © Logitech, 2001-2012 - SV © 2001-2012 Logitech + CS © 2001-2020 Logitech + DA © 2001-2020 Logitech + DE © 2001-2020 Logitech + EN © 2001-2020 Logitech + ES © 2001-2020 Logitech + FI © 2001-2020 Logitech + FR © 2001-2020 Logitech + IT © 2001-2020 Logitech + NL © 2001-2020 Logitech + NO © 2001-2020 Logitech + PL © 2001-2020 Logitech + RU © Logitech, 2001-2020 + SV © 2001-2020 Logitech VERSION - CS Verze 7.9.0 / r%s - DA Version 7.9.0 / r%s - DE Version 7.9.0 / r%s - EN Version 7.9.0 / r%s - ES Versión 7.9.0 / r%s - FI Versio 7.9.0 / r%s - FR Version 7.9.0 / r%s - IT Versione 7.9.0/r%s - NL Versie 7.9.0 / r%s - NO Versjon 7.9.0 / r%s - PL Wersja 7.9.0 / r%s - RU Версия 7.9.0 / r%s - SV Version 7.9.0 / r%s + CS Verze 7.9.4 / r%s + DA Version 7.9.4 / r%s + DE Version 7.9.4 / r%s + EN Version 7.9.4 / r%s + ES Versión 7.9.4 / r%s + FI Versio 7.9.4 / r%s + FR Version 7.9.4 / r%s + IT Versione 7.9.4/r%s + NL Versie 7.9.4 / r%s + NO Versjon 7.9.4 / r%s + PL Wersja 7.9.4 / r%s + RU Версия 7.9.4 / r%s + SV Version 7.9.4 / r%s CHOOSE_A_LANGUAGE CS Zvolte prosím jazyk @@ -301,7 +301,7 @@ PLAYER_VERSION HE גרסת הקושחה של הנגן IT Versione firmware del lettore JA ファームウェア・バージョン - NL Firmwareversie van muzieksysteem + NL Firmwareversie van muziekspeler NO Spillerens fastvareversjon PL Wersja oprogramowania układowego odtwarzacza PT Versão do firmware do cliente @@ -1492,7 +1492,7 @@ NOTHING_CURRENTLY_PLAYING FI Tällä hetkellä ei soi mikään FR Aucun morceau en cours de lecture IT Nessun elemento in corso di riproduzione - NL Er wordt momenteel niets afgespeeld + NL Momenteel wordt niets afgespeeld NO Ingenting spilles for øyeblikket PL Brak aktualnie odtwarzanych elementów RU Сейчас воспроизведение остановлено @@ -1563,7 +1563,7 @@ POWER HE מתח IT Accensione JA パワー - NL Voeding + NL Aan/uit NO Av/på PL Zasilanie RU Питание @@ -1609,7 +1609,7 @@ UNDOCK_PLAYER_PANEL FI Poista soitinpaneelin telakointi FR Détacher le panneau de la platine IT Disancora pannello del lettore - NL Paneel van muzieksysteem ontkoppelen + NL Paneel van muziekspeler ontkoppelen NO Løsne spillerpanel PL Oddokuj okienko odtwarzacza RU Разблокировать панель плеера @@ -1801,7 +1801,7 @@ ALARM_ADD_DESC FI Voit lisätä niin monta herätystä kuin haluat. Jokainen herätys voidaan asettaa käynnistymään joka päivä, vain tiettyinä päivinä, tai vain kerran. Aloita uuden herätyksen lisääminen valitsemalla Lisää herätys. FR Vous pouvez ajouter autant de réveils que vous le désirez. Chaque réveil peut être défini pour se déclencher tous les jours, uniquement certains jours ou une seule fois. Sélectionnez Ajouter un réveil pour débuter l'ajout d'un nouveau réveil. IT È possibile aggiungere il numero di sveglie desiderato. Ciascuna sveglia può essere impostata in modo da suonare tutti i giorni, in giorni specifici oppure solo una volta. Selezionare Aggiungi sveglia per aggiungere una nuova sveglia. - NL Je kunt zoveel weksignalen toevoegen als je wilt. Elk weksignaal kan ingesteld worden om elke dag af te gaan, alleen op specifieke dagen of slechts eenmaal. Selecteer 'Weksignaal toevoegen' om een nieuw signaal toe te voegen. + NL Je kunt zoveel weksignalen toevoegen als je wilt. Elk weksignaal kan ingesteld worden om elke dag af te gaan, alleen op specifieke dagen of slechts eenmaal. Selecteer 'Weksignaal toevoegen' om een nieuw signaal toe te voegen. NO Du kan legge inn så mange vekkinger du ønsker. Hver vekking kan stilles inn til å gå hver dag, på visse dager eller bare én gang. Velg Legg til vekking for å legge til en ny vekking. PL Możesz dodać dowolną liczbę alarmów. Każdy alarm może zostać uruchomiony codziennie, w wybranym dniu lub tylko raz. Aby rozpocząć dodawanie nowego alarmu, wybierz opcję „Dodaj alarm”. RU Можно добавить любое число будильников. Каждый будильник может звонить ежедневно, только по определенным дням или один раз. Щелкните "Добавить будильник", чтобы добавить новый будильник. @@ -1906,7 +1906,7 @@ ALARM_ALARM_ONETIME FI Kertahälytys FR Alarme ponctuelle IT Sveglia non ripetuta - NL Eén alarm + NL Eénmalig alarm NO Engangsvekking PL Alarm jednorazowy RU Одноразовый будильник @@ -2028,7 +2028,7 @@ ALARM_DELETING FI Herätys poistetaan... FR Suppression du réveil... IT Rimozione sveglia... - NL Bezig met verwijderen van weksignaal... + NL Weksignaal wordt verwijderd... NO Fjerner vekking... PL Usuwanie alarmu... RU Удаление будильника... @@ -2917,7 +2917,7 @@ REBUFFERING FR Remise en tampon... HE מבצע אגירה מחדש... IT Rebuffering... - NL Bezig met opnieuw bufferen... + NL Opnieuw bufferen... NO Fyller bufferen på nytt... PL Ponowne buforowanie... RU Повторная буферизация... @@ -3300,7 +3300,7 @@ XML_LINK FR Lien HE קישור IT Collegamento - NL Koppeling + NL Link NO Kopling PL Łącze RU Ссылка @@ -3528,7 +3528,7 @@ SETUP_GROUP_BRIGHTNESS_DESC HE ניתן להגדיר את הבהירות של תצוגת הנגן כאשר הנגן מופעל, כבוי או במצב מוכן (שומר מסך). באפשרותך גם לכוונן את הבהירות באופן אוטומטי כאשר אתה לוחץ על אחד הלחצנים, כאשר מתחיל להשמיע שיר, או כאשר לא מתבצעת כל פעולה שהיא בנגן. באפשרותך לבחור להשבית התנהגות אוטומטית זו. IT È possibile impostare una diversa luminosità del display del lettore a seconda che sia acceso, spento o inattivo (screen saver). La luminosità può essere inoltre regolata automaticamente quando si preme un pulsante, si avvia la riproduzione di un brano o se il lettore è inattivo. Questa funzione automatica può essere disattivata. JA パワーオン・オフ時、スクリーンセーバー時のプレーヤーのディスプレイの明るさを調節できます - NL De helderheid van het muzieksysteemdisplay kan afgestemd worden op de status van het systeem: aan, uit, of in ruststand (schermbeveiliger). De helderheid kan ook automatisch aangepast worden wanneer je op een knop drukt, het afspelen van een nummer start of een tijdje niets doet. Je kunt dit automatische gedrag desgewenst uitschakelen. + NL De helderheid van het display kan afgestemd worden op de status van het systeem: aan, uit, of in ruststand (schermbeveiliger). De helderheid kan ook automatisch aangepast worden wanneer je op een knop drukt, het afspelen van een nummer start of een tijdje niets doet. Je kunt dit automatische gedrag desgewenst uitschakelen. NO Du kan angi ulike innstillinger for lysstyrke avhengig av om spilleren er av, på eller inaktiv (skjermsparer). Dette kan justeres automatisk når du trykker på en knapp, starter avspilling eller lar spilleren stå i fred en stund. Du kan velge å deaktivere disse automatiske justeringene. PL Jasność wyświetlacza odtwarzacza można ustawić dla trybu włączenia, wyłączenia lub bezczynności (wygaszacz ekranu). Można także ustawić ją automatycznie dla naciśnięcia przycisku, rozpoczęcia odtwarzania lub nieużywania przez pewien czas. To automatyczne działanie można wyłączyć. RU Можно настроить яркость экрана плеера во включенном, выключенном или неактивном состоянии (экранная заставка). Она также может настраиваться автоматически, когда пользователь нажимает кнопку, начинает воспроизведение песни или на какое-то время перестает пользоваться устройством. Это автоматическое поведение можно отключить. @@ -3584,7 +3584,7 @@ SETUP_GROUP_TITLEFORMATS_DESC HE אלו הן התבניות הזמינות לבחירה, הן בהגדרות שלעיל והן בהגדרות הנגן. להסרת תבנית, נקה את תיבת הסימון; להזנת תבנית חדשה, הצב אותה בשדה ריק. רכיבי הנתונים הזמינים הם: סוג_תוכן, שם, סגנון, מספר_רצועה (מספר הרצועה כמספר שלם), גודל_קובץ, מבצע, מלחין, מנצח, להקה, אלבום, הערה, שנה, שניות (מספר שניות כולל), משך (דקות ושניות), שינוי_גודל_VBR‏ (vbr/cbr), קצב_סיביות, גודל_תג, אסופה (שם האסופה), נתיב, קובץ, סיומת (סיומת הקובץ), תאריך_ארוך (התאריך הנוכחי, ארוך), תאריך_קצר (התאריך הנוכחי, קצר), שעה_נוכחית. ניתן להפריד בין הרכיבים באמצעות כל דבר (או שום דבר). נעשה שימוש במפרידים רק אם רכיבי הנתונים קיימים.
הבחירה הנוכחית היא התבנית שבה ייעשה שימוש בדפי האינטרנט. IT Questi formati sono selezionabili sia per l'interfaccia Web, sia per i lettori. Per rimuovere un formato, cancellare il testo presente nella casella. Per aggiungere un formato, immettere il testo appropriato nella casella vuota. I dati disponibili sono: CT (tipo di contenuto), TITLE (titolo), GENRE (genere), TRACKNUM (numero di brano sotto forma di numero intero), FS (dimensione file), ARTIST (artista), COMPOSER (compositore), CONDUCTOR (direttore), BAND (gruppo musicale), ALBUM, COMMENT (commento), YEAR (anno), SECS (secondi totali), DURATION (durata in minuti e secondi), BITRATE, LONGDATE (data corrente in formato esteso), SHORTDATE (data corrente in formato abbreviato), CURRTIME (ora corrente). Per gli elementi è possibile utilizzare qualsiasi tipo di separatore o non utilizzare separatori. I separatori vengono utilizzati solo in presenza di dati.
Nelle pagine Web verrà utilizzato il formato correntemente selezionato. JA これらは上記とプレーヤーセットアップに利用できるフォーマットです。フォーマットを削除するには、テキストボックスをクリアし、新しいフォーマットを加えるには空のテキストボックスに入れてください。利用できるデータは: CT(コンテントタイプ)、TITLE(タイトル)、GENRE(ジャンル)、TRACKNUM(トラックナンバー)、FS(ファイルサイズ)、ARTIST(アーチスト)、ALBUM(アルバム)、COMMENT(コメント)、YEAR(年)、SECS(総秒数)、DURATION(長さ)、BITRATE(ビットレート)、LONGDATE(長い日付)、SHORTDATE(短い日付)、CURRTIME(現在の時間)。各データは何で区分してもかまいません。
現在のフォーマット選択はウェブ上で使われているものと同じです。 - NL Dit zijn de indelingen die geselecteerd kunnen worden, zowel in de webinterface als in de instellingen van je muzieksysteem. Als je een indeling wilt verwijderen, maak je het tekstvakje leeg. Wil je een indeling toevoegen, dan zet je het in een leeg vakje. Beschikbare gegevenselementen zijn: CT (inhoudstype), TITLE, GENRE, TRACKNUM (tracknummer als int), FS (bestandsgrootte), ARTIST, COMPOSER, CONDUCTOR, BAND, ALBUM, COMMENT, YEAR, SECS (totaalaantal seconden), DURATION (minuten en seconden), BITRATE, LONGDATE (huidige datum, lang), SHORTDATE (huidige datum, kort), CURRTIME (huidige tijd). De elementen kunnen door alles (of niets) gescheiden geworden. De scheidingstekens worden alleen gebruikt als de gegevenselementen aanwezig zijn.
De huidige selectie is de indeling die op de webpagina's gebruikt zal worden. + NL Dit zijn de indelingen die geselecteerd kunnen worden, zowel in de webinterface als in de instellingen van je muziekspeler. Als je een indeling wilt verwijderen, maak je het tekstvakje leeg. Wil je een indeling toevoegen, dan zet je het in een leeg vakje. Beschikbare gegevenselementen zijn: CT (inhoudstype), TITLE, GENRE, TRACKNUM (nummer als int), FS (bestandsgrootte), ARTIST, COMPOSER, CONDUCTOR, BAND, ALBUM, COMMENT, YEAR, SECS (totaalaantal seconden), DURATION (minuten en seconden), BITRATE, LONGDATE (huidige datum, lang), SHORTDATE (huidige datum, kort), CURRTIME (huidige tijd). De elementen kunnen door alles (of niets) gescheiden geworden. De scheidingstekens worden alleen gebruikt als de gegevenselementen aanwezig zijn.
De huidige selectie is de indeling die op de webpagina's gebruikt zal worden. NO Dette er formatene som er tilgjengelige både i nettgrensesnittet og på spillerne. Tøm boksen for å fjerne et format. Du legger til et nytt format ved å oppgi det i en tom boks. Tilgjengelige dataelementer er CT (innholdstype), TITLE (tittel), GENRE (sjanger), TRACKNUM (spornummer), FS (filstørrelse), ARTIST, COMPOSER (komponist), CONDUCTOR (dirigent), BAND, ALBUM, COMMENT (kommentar), YEAR (år), SECS (totalt antall sekunder, oppgitt som et heltall), DURATION (minutter og sekunder), BITRATE, LONGDATE (dagens dato i langt format), SHORTDATE (dagens dato i kort format) og CURRTIME (klokkeslett nå). Elementer kan separeres med ethvert tegn, eller ingen. Skilletegnet vises bare om dataelementet finnes.
Det valgte formatet er det som blir brukt på nettsidene. PL Są to formaty dostępne do wyboru w interfejsie internetowym i w odtwarzaczach. Aby usunąć format, wyczyść pole tekstowe; aby wprowadzić nowy format, wpisz go w pustym polu. Dostępne elementy danych to: CT (typ zawartości), TITLE, GENRE, TRACKNUM (numer utworu jako liczba rzeczywista), FS (rozmiar pliku), ARTIST, COMPOSER, CONDUCTOR, BAND, ALBUM, COMMENT, YEAR, SECS (całkowita liczba sekund), DURATION (minuty i sekundy), LONGDATE (bieżąca data, długa), SHORTDATE (bieżąca data, krótka), CURRTIME (bieżąca godzina). Elementy mogą być rozdzielone dowolnym znakiem (lub nierozdzielone). Separatory są używane tylko wtedy, gdy występują elementy danych.
Aktualny wybór to format, który będzie używany na stronach internetowych. PT Aqui poderá definir o formato da descrição da música que aparece acima e no cliente. Para remover um campo, limpe a caixa do texto. Para adicionar um campo, adicione-o numa caixa vazia. Os campos disponíveis são @@ -3603,7 +3603,7 @@ SETUP_PLAYER_TITLEFORMATS_DESC HE אלו הן התבניות הזמינות לבחירה, הן בהגדרות שלעיל והן בהגדרות הנגן. להסרת תבנית, נקה את תיבת הסימון; להזנת תבנית חדשה, הצב אותה בשדה ריק. רכיבי הנתונים הזמינים הם: סוג_תוכן, שם, סגנון, מספר_רצועה (מספר הרצועה כמספר שלם), גודל_קובץ, מבצע, מלחין, מנצח, להקה, אלבום, הערה, שנה, שניות (מספר שניות כולל), משך (דקות ושניות), שינוי_גודל_VBR‏ (vbr/cbr), קצב_סיביות, גודל_תג, אסופה (שם האסופה), נתיב, קובץ, סיומת (סיומת הקובץ), תאריך_ארוך (התאריך הנוכחי, ארוך), תאריך_קצר (התאריך הנוכחי, קצר), שעה_נוכחית. ניתן להפריד בין הרכיבים באמצעות כל דבר (או שום דבר). נעשה שימוש במפרידים רק אם רכיבי הנתונים קיימים.
הבחירה הנוכחית היא התבנית שבה ייעשה שימוש בדפי האינטרנט. IT Questi formati sono selezionabili sia per l'interfaccia Web, sia per i lettori. Per rimuovere un formato, cancellare il testo presente nella casella. Per aggiungere un formato, immettere il testo appropriato nella casella vuota. I dati disponibili sono: CT (tipo di contenuto), TITLE (titolo), GENRE (genere), TRACKNUM (numero di brano sotto forma di numero intero), FS (dimensione file), ARTIST (artista), COMPOSER (compositore), CONDUCTOR (direttore), BAND (gruppo musicale), ALBUM, COMMENT (commento), YEAR (anno), SECS (secondi totali), DURATION (durata in minuti e secondi), BITRATE, LONGDATE (data corrente in formato esteso), SHORTDATE (data corrente in formato abbreviato), CURRTIME (ora corrente). Per gli elementi è possibile utilizzare qualsiasi tipo di separatore o non utilizzare separatori. I separatori vengono utilizzati solo in presenza di dati.
Nelle pagine Web verrà utilizzato il formato correntemente selezionato. JA これらは上記とプレーヤーセットアップに利用できるフォーマットです。フォーマットを削除するには、テキストボックスをクリアし、新しいフォーマットを加えるには空のテキストボックスに入れてください。利用できるデータは: CT(コンテントタイプ)、TITLE(タイトル)、GENRE(ジャンル)、TRACKNUM(トラックナンバー)、FS(ファイルサイズ)、ARTIST(アーチスト)、ALBUM(アルバム)、COMMENT(コメント)、YEAR(年)、SECS(総秒数)、DURATION(長さ)、BITRATE(ビットレート)、LONGDATE(長い日付)、SHORTDATE(短い日付)、CURRTIME(現在の時間)。各データは何で区分してもかまいません。
現在のフォーマット選択はウェブ上で使われているものと同じです。 - NL Dit zijn de indelingen die geselecteerd kunnen worden, zowel in de webinterface als in de instellingen van je muzieksysteem. Als je een indeling wilt verwijderen, maak je het tekstvakje leeg. Wil je een indeling toevoegen, dan zet je het in een leeg vakje. Beschikbare gegevenselementen zijn: CT (inhoudstype), TITLE, GENRE, TRACKNUM (tracknummer als int), FS (bestandsgrootte), ARTIST, COMPOSER, CONDUCTOR, BAND, ALBUM, COMMENT, YEAR, SECS (totaalaantal seconden), DURATION (minuten en seconden), BITRATE, LONGDATE (huidige datum, lang), SHORTDATE (huidige datum, kort), CURRTIME (huidige tijd). De elementen kunnen door alles (of niets) gescheiden geworden. De scheidingstekens worden alleen gebruikt als de gegevenselementen aanwezig zijn.
De huidige selectie is de indeling die op de webpagina's gebruikt zal worden. + NL Dit zijn de indelingen die geselecteerd kunnen worden, zowel in de webinterface als in de instellingen van je muziekspeler. Als je een indeling wilt verwijderen, maak je het tekstvakje leeg. Wil je een indeling toevoegen, dan zet je het in een leeg vakje. Beschikbare gegevenselementen zijn: CT (inhoudstype), TITLE, GENRE, TRACKNUM (nummer als int), FS (bestandsgrootte), ARTIST, COMPOSER, CONDUCTOR, BAND, ALBUM, COMMENT, YEAR, SECS (totaalaantal seconden), DURATION (minuten en seconden), BITRATE, LONGDATE (huidige datum, lang), SHORTDATE (huidige datum, kort), CURRTIME (huidige tijd). De elementen kunnen door alles (of niets) gescheiden geworden. De scheidingstekens worden alleen gebruikt als de gegevenselementen aanwezig zijn.
De huidige selectie is de indeling die op de webpagina's gebruikt zal worden. NO Dette er formatene som er tilgjengelige både i nettgrensesnittet og på spillerne. Tøm boksen for å fjerne et format. Du legger til et nytt format ved å oppgi det i en tom boks. Tilgjengelige dataelementer er CT (innholdstype), TITLE (tittel), GENRE (sjanger), TRACKNUM (spornummer), FS (filstørrelse), ARTIST, COMPOSER (komponist), CONDUCTOR (dirigent), BAND, ALBUM, COMMENT (kommentar), YEAR (år), SECS (totalt antall sekunder, oppgitt som et heltall), DURATION (minutter og sekunder), BITRATE, LONGDATE (dagens dato i langt format), SHORTDATE (dagens dato i kort format) og CURRTIME (klokkeslett nå). Elementer kan separeres med ethvert tegn, eller ingen. Skilletegnet vises bare om dataelementet finnes.
Det valgte formatet er det som blir brukt på nettsidene. PL Są to formaty dostępne do wyboru w odtwarzaczu. Aby wprowadzić nowy format do wyboru na odtwarzaczu, wybierz jeden z listy rozwijanej i zapisz ustawienia.
Aktualny wybór to format, który będzie używany na odtwarzaczu.
Jeżeli chcesz dodać nowy format, niedostępny w liscie rozwijanej, przejdź do: Ustawienia serwera > Interfejs > Format tytułu PT Aqui poderá definir o formato da descrição da música que aparece acima e no cliente. Para remover um campo, limpe a caixa do texto. Para adicionar um campo, adicione-o numa caixa vazia. Os campos disponíveis são @@ -3695,7 +3695,7 @@ SETUP_ACTIVEFONT_DESC HE באפשרותך להשתמש בלחצן SIZE (גודל) בשלט-רחוק או בלחצני האפשרויות שלהלן למעבר בין גופנים שונים בתצוגת הנגן. באפשרותך לבחור את סדר הופעתם באמצעות התפריטים שלהלן. IT Per cambiare la dimensione del carattere visualizzato sul display del lettore, utilizzare il pulsante Size del telecomando o i pulsanti di opzione sottostanti. Tramite il menu sottostante è possibile scegliere l'ordine in cui vengono visualizzati. JA リモコンのSIZEボタンを押してフォントのサイズを変えることができます。サイズが変わる順序を以下のメニューで指定できます。 - NL Je kunt de knop SIZE op de afstandsbediening, of de keuzerondjes hieronder gebruiken om tussen lettertypen op het display van het muzieksysteem te schakelen. Via de onderstaande menu's kun je bepalen in welke volgorde ze verschijnen. + NL Je kunt de knop SIZE op de afstandsbediening, of de keuzerondjes hieronder gebruiken om tussen lettertypen op het display van de muziekspeler te schakelen. Via de onderstaande menu's kun je bepalen in welke volgorde ze verschijnen. NO Du kan bruke SIZE-knappen på fjernkontrollen eller knappene under for å velge mellom ulike skrifttyper for spillerens skjerm. I menyene nedenfor kan du også stille inn i hvilken rekkefølge skrifttypene vises. PL Przycisk SIZE na pilocie lub przyciski opcji poniżej umożliwiają przełączanie czcionek na wyświetlaczu odtwarzacza. Możliwe jest wybranie kolejności wyświetlania za pomocą menu poniżej. RU С помощью кнопки SIZE пульта ДУ или нижеприведенных переключателей можно переключаться между шрифтами экрана плеера. Порядок их вывода можно задать с помощью следующих меню. @@ -3731,7 +3731,7 @@ SETUP_IDLEFONT_DESC HE כאשר הנגן נמצא במצב המתנה, באפשרותך להשתמש בלחצן SIZE (גודל) בשלט-רחוק או בלחצני האפשרויות שלהלן למעבר בין גופנים שונים בתצוגת השעון. באפשרותך לבחור את סדר הופעתם באמצעות התפריטים שלהלן. IT Quando il lettore è in standby, è possibile utilizzare il pulsante SIZE del telecomando o i pulsanti di opzione di seguito per scegliere i caratteri da utilizzare per la visualizzazione dell'orologio. Per impostare l'ordine in cui sono visualizzati, utilizzare i menu riportati di seguito. JA リモコンのSIZEボタンを押して、プレーヤーがスタンバイ状態時の時計機能のフォントサイズを変えることができます。サイズが変わる順序を以下のメニューで指定できます。 - NL Je kunt de knop SIZE op de afstandsbediening, of de keuzerondjes hieronder gebruiken om tussen lettertypen voor de klokdisplay te schakelen wanneer het muzieksysteem stand-by staat. Via de onderstaande menu's kun je bepalen in welke volgorde ze verschijnen. + NL Je kunt de knop SIZE op de afstandsbediening, of de keuzerondjes hieronder gebruiken om tussen lettertypen voor de klokdisplay te schakelen wanneer de muziekspeler stand-by staat. Via de onderstaande menu's kun je bepalen in welke volgorde ze verschijnen. NO Når spilleren er i hvilemodus, kan du bruke SIZE-knappen på fjernkontrollen eller knappene under for å velge mellom ulike skrifttyper for klokkeskjermen. I menyene nedenfor kan du også stille inn i hvilken rekkefølge skrifttypene vises. PL Gdy odtwarzacz działa w trybie gotowości, można użyć przycisku SIZE na pilocie lub przycisków opcji poniżej, aby zmienić czcionki wyświetlania zegara. Kolejność ich wyświetlania można wybrać z menu poniżej. RU Когда плеер находится в режиме ожидания, с помощью кнопки SIZE на пульте ДУ или расположенных ниже переключателей можно изменять размеры шрифта экрана часов. Следующие меню позволяют выбрать порядок их отображения. @@ -3768,7 +3768,7 @@ SETUP_DOUBLESIZE_DESC HE בחירת גודל הטקסט עבור תצוגת הנגן IT Scegli la dimensione del testo per il display del lettore JA プレーヤーディスプレイのテキストサイズを選んでください - NL Een tekstgrootte voor het scherm van het muzieksysteem kiezen + NL Een tekstgrootte voor het scherm van de muziekspeler kiezen NO Velg tekststørrelse for spillerens skjerm PL Wybierz rozmiar tekstu na wyświetlaczu odtwarzacza RU Выберите размер текста для экрана плеера @@ -3823,7 +3823,7 @@ SETUP_SCROLLPAUSE_DESC HE באפשרותך לכוונן את משך הזמן שבו הנגן ממתין לפני שמתחיל בגלילה אופקית של טקסט בתצוגה. הזן ערך, בשניות, עבור פרק הזמן שבו יש להמתין לפני גלילה או בין גלילות. הזן אפס כדי שלא לקיים השהייה כלל ולגלילה מיידית. ברירת המחדל היא 3.6 שניות. IT È possibile impostare il tempo che il lettore attende prima di iniziare lo scorrimento orizzontale del testo sullo schermo. Inserire un valore in secondi per la quantità di tempo da attendere prima di iniziare lo scorrimento e fra uno scorrimento e l'altro. Inserire zero per non attendere ed iniziare immediatamente lo scorrimento. Il valore predefinito è 3,6 secondi. JA ディスプレイスクロール開始までの時間を調節することができます。秒数を入力してください。「0」とすると、すぐに開始します。出荷時設定は3.6秒です。 - NL Je kunt aanpassen hoe lang het muzieksysteem wacht voordat het lopen van de tekst op het display start. Voer een waarde in (in seconden) die aangeeft hoe lang er gewacht moet worden voordat de tekst gaat lopen, of voordat de tekst herhaald wordt. Voer 0 in om helemaal niet te wachten en de tekst direct te laten lopen. De standaardinstelling is 3,6 seconden. + NL Je kunt aanpassen hoe lang de muziekspeler wacht voordat het lopen van de tekst op het display start. Voer een waarde in (in seconden) die aangeeft hoe lang er gewacht moet worden voordat de tekst gaat lopen, of voordat de tekst herhaald wordt. Voer 0 in om helemaal niet te wachten en de tekst direct te laten lopen. De standaardinstelling is 3,6 seconden. NO Du kan justere hvor lenge spilleren venter før den starter sidelengs tekstrulling på skjermen. Angi hvor mange sekunder den skal vente før den ruller, eller mellom rulling. Skriv null hvis du ikke vil ha noen pause, men heller vil at teksten skal begynne å rulle med én gang. Standard er 3,6 sekunder. PL Czas, po jakim odtwarzacz rozpoczyna przewijanie tekstu w poziomie na ekranie odtwarzacza można dostosować. Wprowadź wartość oczekiwania w sekundach przed rozpoczęciem przewijania lub między kolejnymi przewijaniami. Wartość zero wyłącza oczekiwanie i powoduje natychmiastowe przewijanie. Wartość domyślna to 3,6 sekundy. RU Можно задать время, по истечении которого плеер начинает горизонтальную прокрутку текста на экране. Введите значение в секундах для времени ожидания до начала прокрутки или между прокрутками. Для немедленной прокрутки без пауз введите «0». Значение по умолчанию — 3,6 с. @@ -3859,7 +3859,7 @@ SETUP_SCROLLRATE_DESC HE באפשרותך לכוונן את הקצב שבו טקסט נגלל נע על-פני המסך. קצב הגלילה מגדיר את משך הזמן בין עדכוני התצוגה. בנגן עם תצוגה גרפית, ניתן לכוונן הגדרה זו בשילוב עם ההגדרה של פיקסלים לגלילה כדי להגדיר את המהירות והכמות של הגלילה (הטקסט ינוע לפי 'פיקסלים לגלילה' בכל מספר השניות המוגדר ב'קצב גלילה'). בנגן עם תצוגת תווים, הגדרה זו קובעת את משך הזמן לפני הגלילה לתו הבא. הזן ערך, בשניות, עבור פרק הזמן בין עדכונים. ככל שהמספר נמוך יותר, הגלילה מהירה יותר. ערך ברירת המחדל הוא 0.15 עבור שורה אחת; 0.1 שניות עבור שורה כפולה. IT È possibile regolare la velocità di scorrimento del testo sullo schermo, che definisce l'intervallo di aggiornamento dello schermo. In un lettore con display grafico, può essere regolata insieme all'impostazione Pixel scorrimento per impostare la velocità e la qualità dello scorrimento (il testo si sposta del numero di pixel specificato una volta trascorso l'intervallo di tempo impostato per la velocità di scorrimento). In un lettore con un display a caratteri, il valore definisce l'intervallo di tempo che trascorre tra la visualizzazione di un carattere e la visualizzazione di quello successivo. Immettere un valore espresso in secondi. Più basso è il valore, più veloce sarà lo scorrimento. Il valore predefinito è di 0,033 secondi. JA ディスプレイテキストの別の文字をスクロール開始するまでの時間を調節することができます。秒数を入力してください。出荷時設定は、1行フォントであれば0.15秒、2行フォントであれば0.10秒です。 - NL Je kunt de snelheid instellen waarmee tekst over het scherm loopt. De snelheid geeft de tijd tussen displayupdates aan. Op een muzieksysteem met grafisch display kan dit gespecificeerd worden in combinatie met 'Tekstlooppixels' om de snelheid en kwaliteit van de tekstloop in te stellen (met 'Tekstlooppixels' wordt de tekst telkens volgens het aantal seconden in 'Tekstloopsnelheid' verplaatst). Op een muzieksysteem met een tekendisplay geeft dit aan hoe lang het duurt voordat het volgende teken verschijnt. Voer een waarde, in seconden, in voor de tijd tussen updates. Lagere waarden laten de tekst sneller lopen. De standaardwaarde is 0,033 seconden. + NL Je kunt de snelheid instellen waarmee tekst over het scherm loopt. De snelheid geeft de tijd tussen displayupdates aan. Op een muziekspeler met grafisch display kan dit gespecificeerd worden in combinatie met 'Tekstlooppixels' om de snelheid en kwaliteit van de tekstloop in te stellen (met 'Tekstlooppixels' wordt de tekst telkens volgens het aantal seconden in 'Tekstloopsnelheid' verplaatst). Op een muziekspeler met een tekendisplay geeft dit aan hoe lang het duurt voordat het volgende teken verschijnt. Voer een waarde, in seconden, in voor de tijd tussen updates. Lagere waarden laten de tekst sneller lopen. De standaardwaarde is 0,033 seconden. NO Du kan justere hastigheten for rullende tekst på skjermen. Rullefrekvensen angir tiden mellom oppdateringer av skjermen. På en spiller med grafisk skjerm kan denne innstillingen benyttes sammen med Rullepiksler for å styre rullehastighet og kvalitet. (Teksten vil rulle i henhold til Rullepiksler for hvert intervall angitt under Rullefrekvens.) På en spiller med tegnskjerm vil dette angi tiden før rulling til neste tegn. Skriv inn en verdi i sekunder for tiden mellom oppdateringer. Lavere tall vil gjøre rullingen raskere. 0,033 sekunder er standard. PL Możliwe jest dostosowanie szybkości przewijania tekstu na ekranie. Szybkość przewijania określa czas między aktualizacjami zawartości wyświetlacza. W przypadku odtwarzacza z wyświetlaczem graficznym parametr ten można dostosować razem z opcją Przewijane piksele w celu ustawienia szybkości i jakości przewijania (tekst będzie przesuwany o wartość ustawienia Przewijane piksele co liczbę sekund określoną w opcji Szybkość przewijania). W przypadku odtwarzacza z wyświetlaczem znakowym parametr ten oznacza czas przed przewinięciem do następnego znaku. Wprowadź wartość czasu między aktualizacjami w sekundach. Mniejsze wartości powodują szybsze przewijanie. 0,15 to wartość domyślna. RU Можно настроить скорость прокрутки теста на экране. Параметр «Скорость прокрутки» определяет промежуток времени между обновлениями экрана. На плеерах с графическим экраном он настраивается совместно с параметром «Число пикселей прокрутки», при этом задается скорость и качество прокрутки (текст будет перемещаться на число пикселей прокрутки через каждые несколько секунд скорости прокрутки). На плеерах с символьным экраном этот параметр определяет промежуток времени перед прокруткой до следующего символа. Чтобы задать промежуток времени между обновлениями, введите значение в секундах. Чем меньше число, тем выше скорость. Значение по умолчанию 0,033 с. @@ -3911,7 +3911,7 @@ SETUP_GROUP_IRSETS_DESC HE באפשרותך לבחור להורות לנגן להגיב לאותות אינפרה-אדום מסוימים או להתעלם מהם. כדי להפעיל את האפשרות לקביעת קוד עבור השלט-רחוק, הצב סימן ביקורת ליד השם שלהלן. כדי להשבית את האפשרות לקביעת קוד עבור השלט-רחוק, נקה סימן הביקורת. IT È possibile impostare il lettore in modo che risponda a specifici comandi a infrarossi o li ignori. Per attivare un set di comandi, selezionare la casella in corrispondenza del nome indicato di seguito; per disattivarlo, deselezionarla. JA 特定のリモートコントロール信号に反応または無視するように設定できます。以下、チェックすると有効に、チェックを消すと無効になります。 - NL Je kunt instellen op welke infraroodsignalen het muzieksysteem moet reageren, en welke het moet negeren. Selecteer hieronder een naam om een codeset voor de afstandsbediening te activeren. Haal het vinkje weg om de codeset te deactiveren. + NL Je kunt instellen op welke infraroodsignalen de muziekspeler moet reageren, en welke het moet negeren. Selecteer hieronder een naam om een codeset voor de afstandsbediening te activeren. Haal het vinkje weg om de codeset te deactiveren. NO Du kan velge om denne spilleren skal reagere på eller ignorere visse infrarøde signaler fra fjernkontrollen. Du kan aktivere et kodesett for en fjernkontroll ved å merke av ved navnet nedenfor. Du kan deaktivere et kodesett ved å fjerne avmerkingen. PL Odtwarzacz można ustawić tak, aby reagował na określone sygnały pilota na podczerwień lub je ignorował. Aby włączyć zestaw kodów pilota zdalnego sterowania, zaznacz pole wyboru obok nazwy poniżej. Aby wyłączyć zestaw kodów, usuń zaznaczenie. PT Pode optar por permitir ou negar alguns sinais do controlo remoto no seu cliente. Para activar um sinal, active a opção junto ao nome abaixo. Para desactivar, limpe a opção. @@ -3943,7 +3943,7 @@ SETUP_GROUP_PLUGINS_DESC FI Logitech Media Server sisältää laajennuksia, joita voi käyttää kaukosäätimellä tai internet-liittymän välityksellä. Yksittäiset laajennukset voidaan ottaa käyttöön tai poistaa käytöstä alla.

Huomaa: Sovellus täytyy käynnistää uudelleen, jotta muutosten vaikutukset näkyvät. FR Le Logitech Media Server inclut certains plugins à utiliser avec la télécommande ou à partir de l'interface Web. Vous pouvez activer ou désactiver des plugins ci-dessous.

Remarque: Vous devez redémarrer le serveur pour que les modifications prennent effet. IT In Logitech Media Server sono disponibili alcuni plugin utilizzabili con il telecomando o nell'interfaccia Web. È possibile attivare o disattivare i singoli plugin sottostanti.

Nota: per rendere effettive le modifiche, è necessario riavviare l'applicazione. - NL Logitech Media Server bevat een aantal plug-ins die je met je afstandsbediening of in de webinterface kunt gebruiken. Met de onderstaande knoppen kun je afzonderlijke plug-ins in- of uitschakelen.

N.B. Je moet Logitech Media Server opnieuw opstarten om de wijzigingen door te voeren. + NL Logitech Media Server bevat een aantal plug-ins die je met je afstandsbediening of in de webinterface kunt gebruiken. Met de onderstaande knoppen kun je afzonderlijke plug-ins in- of uitschakelen.

N.B. Je moet Logitech Media Server herstarten om de wijzigingen door te voeren. NO Logitech Media Server har plugin-moduler som du kan bruke med fjernkontrollen, eller i nettgrensesnittet. Du kan velge å aktivere eller deaktivere plugin-moduler enkeltvis nedenfor.

Merk: Du må starte programmet på nytt før disse endringene trer i kraft. PL Program Logitech Media Server zawiera dodatki, których można używać za pomocą pilota lub w interfejsie internetowym. Poszczególne dodatki można włączyć lub wyłączyć poniżej.

Uwaga: aby zobaczyć efekt wprowadzenia zmian, należy ponownie uruchomić aplikację. RU Logitech Media Server содержит несколько подключаемых модулей, которыми можно управлять с помощью пульта ДУ или веб-интерфейса. Можно включать и выключать отдельные подключаемые модули.

Примечание: чтобы увидеть результат изменений, перезагрузите приложение. @@ -3958,7 +3958,7 @@ SETUP_GROUP_PLUGINS_NEEDS_RESTART FI Muutoksen voimaantulo edellyttää uudelleenkäynnistystä. FR La modification requiert un redémarrage pour être appliquée. IT Per applicare la modifica è necessario riavviare. - NL De wijziging wordt pas van kracht na opnieuw starten. + NL Wijziging wordt na een herstart van kracht. NO Du må starte på nytt før denne endringen trer i kraft. PL Wprowadzenie zmiany wymaga ponownego uruchomienia. RU Изменения вступят в силу после перезапуска. @@ -4053,7 +4053,7 @@ SETUP_GROUP_FORMATS_CONVERSION_DESC HE Logitech Media Server יכול להמיר תבניות של קובצי שמע במהלך השמעה של מוסיקה בנגן. באפשרותך להשבית את ההפעלה של תבניות ספציפיות מהרשימה שלהלן באמצעות ביטול הסימון שלהן. כאן מוצג רק השם של הנתונים הבינאריים. כדי להציג/לערוך את כל שורת הפקודה, יהיה עליך לפתוח את convert.conf. לחץ על 'שנה' לשמירת השינויים. IT Logitech Media Server consente di convertire rapidamente i formati dei file audio per eseguirne la riproduzione tramite il lettore. È possibile disattivare i formati riportati di seguito deselezionandoli. Qui è visualizzato solo il nome del file binario. Per visualizzare/modificare l'intera riga di comando, è necessario aprire il file convert.conf. Per salvare le modifiche, fare clic su Cambia. JA Logitech Media Serverはオーディオファイル フォーマットを自動的に変換することができます。特定のフォーマットを無効にするにはチェックマークを消してください。フォーマット名のみが出ていますが、コマンドライン全てを見るまたは編集するには、convert.conf を開いてください。変更をセーブするには「変更」をクリックして下さい。 - NL Logitech Media Server kan audioformaten direct tijdens het afspelen converteren. Je kunt specifieke formaten hieronder uitschakelen door het vinkje weg te halen. Alleen de naam van het programmabestand wordt weergegeven. Als je de hele opdrachtregel wilt zien, moet je 'convert.conf' openen. Klik op 'Veranderen' om je wijzigingen op te slaan. + NL Logitech Media Server kan audioformaten direct tijdens het afspelen converteren. Je kunt specifieke formaten hieronder uitschakelen door het vinkje weg te halen. Alleen de naam van het programmabestand wordt weergegeven. Als je de hele opdrachtregel wilt zien, moet je 'convert.conf' openen. Klik op 'Wijzigen' om je instellingen op te slaan. NO Logitech Media Server kan konvertere lydfilformater på direkten, slik at filene kan spilles av på spilleren. Du kan deaktivere formater nedenfor, ved å fjerne markeringen ved dem. Bare binærnavnet vises her. Hvis du vil se/redigere hele kommandolinjen, må du åpne convert.conf. Klikk Endre for å lagre endringene. PL Program Logitech Media Server może przekonwertować formaty plików audio w locie w celu umożliwienia odtworzenia. Poszczególne formaty można wyłączyć poniżej, usuwając zaznaczenie pól wyboru. W tym miejscu widoczna jest tylko nazwa pliku binarnego. Aby wyświetlić/edytować tekst w całym wierszu polecenia, należy otworzyć plik convert.conf. Kliknij przycisk Zmień, aby zapisać zmiany. RU Logitech Media Server может в реальном времени преобразовывать форматы аудиофайлов для воспроизведения на плеере. Можно отключить те или иные форматы, сняв соответствующие флажки. Здесь отображается только имя двоичного файла. Чтобы просмотреть или изменить всю командную строку, нужно открыть файл convert.conf. Щелкните "Изменить", чтобы сохранить изменения. @@ -4071,7 +4071,7 @@ SETUP_FORMATSLIST_MISSING_BINARY HE הנתונים הבינאריים הנחוצים לא נמצאו: IT Il file binario richiesto non è stato trovato: JA ご指定のフォーマットが見つかりません: - NL Vereist programma is niet gevonden: + NL Vereiste programma is niet gevonden: NO Finner ikke påkrevd program: PL Wymagany plik binarny nie został znaleziony: RU Требуемый двоичный файл не найден: @@ -4218,7 +4218,7 @@ SETUP_DISABLEDEXTENSIONSPLAYLIST FR Types de liste de lecture ignorés HE סיומות קבצים מושבתות של רשימות השמעה IT Estensioni file di playlist ignorate - NL Uitgeschakelde playlistbestandsextensies + NL Uitgeschakelde playlist-extensies NO Deaktiverte filendelser for spillelister PL Rozszerzenia plików wyłączone na liście odtwarzania RU Отключенные расширения файлов плей-листа @@ -4253,7 +4253,7 @@ PLAYER_TYPE HE סוג הנגן IT Tipo lettore JA プレーヤー タイプ - NL Type muzieksysteem + NL Type muziekspeler NO Spillertype PL Typ odtwarzacza RU Тип плеера @@ -4271,7 +4271,7 @@ PLAYER_ID HE מזהה נגן IT Identificativo lettore JA プレーヤーID - NL Muzieksysteem-ID + NL Muziekspeler-ID NO Spiller-ID PL Identyfikator odtwarzacza RU ID плеера @@ -4337,7 +4337,7 @@ SETUP_PLAYERNAME HE שם הנגן IT Nome Lettore JA プレーヤー名 - NL Naam van muzieksysteem + NL Naam van muziekspeler NO Spillernavn PL Nazwa odtwarzacza PT Nome do Cliente @@ -4356,7 +4356,7 @@ SETUP_PLAYERNAME_DESC HE באפשרותך להעניק לנגן שם שישמש לזיהוי הנגן בדפי אינטרנט אלה. IT È possibile assegnare al lettore un nome da utilizzare per l'identificazione del lettore su queste pagine Web. JA 複数のプレーヤーを使う場合、それぞれに名前を付けて区別することができます。 - NL Je kunt dit muzieksysteem een naam geven zodat het op deze webpagina's geïdentificeerd kan worden. + NL Je kunt deze muziekspeler een naam geven om hem van andere muziekspelers te onderscheiden. NO Du kan gi denne spilleren et navn som brukes til å identifisere den på disse nettsidene. PL Odtwarzaczowi można nadać nazwę, która będzie go identyfikować na tych stronach internetowych. PT Pode escolher um nome para identificar este cliente nas páginas web do servidor. @@ -4373,7 +4373,7 @@ SETUP_RESET_PLAYER FI Palauta soittimen määritykset FR Réinitialiser les préférences de platine IT Reimposta preferenze lettore - NL Voorkeuren van muzieksysteem opnieuw instellen + NL Voorkeuren van muziekspeler opnieuw instellen NO Tilbakestill spillerinnstillinger PL Resetuj preferencje odtwarzacza RU Сброс настроек плеера @@ -4388,7 +4388,7 @@ SETUP_RESET_PLAYER_DESC FI Voit palauttaa soittimen asetukset, esimerkiksi nimen, nimen muodon tai valikot tehdasasetuksiin. Huomaa, että tämä ei palauta soittimen laitteistoasetuksia, esimerkiksi verkkoasetuksia. FR Vous pouvez réinitialiser les paramètres d'une platine tels que le nom de platine, le format de titre, les menus, etc., pour rétablir les paramètres d'usine par défaut. Notez que cela ne réinitialisera pas les paramètres matériels de la platine tels que les paramètres réseau. IT È possibile ripristinare le impostazioni di fabbrica di un lettore, quali nome, formato del titolo e così via. Questa operazione non ripristina le impostazioni hardware del lettore, quali i parametri di rete. - NL Je kunt de muzieksysteeminstellingen, zoals spelernaam, titelformaat, menu's, enz., op de fabriekswaarden terugzetten. De hardware-instellingen van het systeem, zoals netwerkparameters, worden niet teruggezet. + NL Je kunt de muziekspelerinstellingen, zoals spelernaam, titelformaat, menu's, enz., op de fabriekswaarden terugzetten. De hardware-instellingen van het systeem, zoals netwerkparameters, worden niet teruggezet. NO Du kan tilbakestille spillerinnstillinger som spillernavn, tittelformat, menyer osv. til fabrikkinnstillingene. Dette tilbakestiller ikke spillerens maskinvareinnstillinger, som for eksempel nettverksparametre. PL Możliwe jest przywrócenie ustawień odtwarzacza, takich jak nazwa odtwarzacza, format tytułu, menu itp. do domyślnych wartości fabrycznych. Należy pamiętać, że nie spowoduje to zresetowania ustawień sprzętowych odtwarzacza, np. parametrów sieciowych. RU Можно сбросить такие настройки плеера, как название, формат названия, меню и др., вернув заводские значения по умолчанию. Обратите внимание, что аппаратные настройки (например, параметры сети) сброшены не будут. @@ -4403,7 +4403,7 @@ SETUP_RESET_PLAYER_CONFIRM FI Haluatko varmasti palauttaa soittimen tehdasasetuksiin? FR Etes-vous sûr de vouloir rétablir les paramètres par défaut de votre platine? IT Ripristinare le impostazioni di fabbrica del lettore? - NL Wilt je je muzieksysteem echt terugzetten op de beginwaarden? + NL Wil je je muziekspeler echt terugzetten op de beginwaarden? NO Er du sikker på at du vil tilbakestille spilleren til fabrikkinnstillingene? PL Czy na pewno chcesz przywrócić domyślne ustawienia fabryczne odtwarzacza? RU Вернуть заводские настройки плеера по умолчанию? @@ -4633,7 +4633,7 @@ SETUP_PLAYINGDISPLAYMODE_DESC HE הנגן יכול להציג מידע אודות התקדמות השיר במהלך ההשמעה. בחר את המידע שברצונך להציג. IT È possibile visualizzare nel lettore informazioni sulla riproduzione in corso. Scegliere le informazioni che si desidera visualizzare. JA プレーヤーは演奏中の曲の進行具合を表示することができます。表示したい情報を選んでください。 - NL Het muzieksysteem kan informatie over de voortgang van een nummer weergeven, terwijl het afgespeeld wordt. Kies de informatie die weergegeven moet worden. + NL De muziekspeler kan informatie over de voortgang van een nummer weergeven, terwijl het afgespeeld wordt. Kies de informatie die weergegeven moet worden. NO Spilleren kan vise hvor mye av sangen som er spilt av under avspillingen. Du kan selv velge hvilken informasjon du vil se som standard, og aktivere og deaktivere moduser. Du kan bla gjennom disse modusene ved å trykke på NOW PLAYING på fjernkontrollen flere ganger. PL Odtwarzacz umożliwia wyświetlanie informacji o postępie odtwarzania utworu. Możliwe jest wybranie informacji wyświetlanych domyślnie oraz włączenie lub wyłączenie trybów, które mają być dostępne. Aby przełączać się między tymi trybami, należy naciskać przycisk Now Playing na pilocie. PT O cliente pode mostrar informação do progresso da música enquanto toca. Escolha a informação que deseja que seja mostrada. @@ -4671,7 +4671,7 @@ SETUP_SYNCHRONIZE_DESC HE ניתן לסנכרן את הנגן עם נגנים אחרים, ולאפשר להם להשמיע את אותה מוסיקה בו-זמנית. בחר את הנגנים שאותם ברצונך לסנכרן מהרשימה של קבוצות הסינכרון הזמינות. בחר 'ללא סינכרון' כדי לעצור את הסינכרון. IT Il lettore può essere sincronizzato con altri lettori, in modo da riprodurre la stessa musica contemporaneamente. Scegliere il lettore con il quale ci si vuole sincronizzare dall'elenco gruppi di dispositivi disponibili. Scegliere Nessuna sincronizzazione per interrompere la sincronizzazione. JA プレーヤーは他のプレーヤーとシンクロし、同じ曲を同時に再生することができます。シンクロさせるプレーヤーを選ぶか、シンクロなしを選んでください。 - NL Het muzieksysteem kan met andere muzieksystemen gesynchroniseerd worden. De systemen geven dan dezelfde muziek weer. Kies de systemen waarmee gesynchroniseerd moet worden uit de lijst van beschikbare synchronisatiegroepen. Kies 'Geen synchronisatie' om deze functie te annuleren. + NL De muziekspeler kan met andere muziekspelers gesynchroniseerd worden. De systemen geven dan dezelfde muziek weer. Kies de systemen waarmee gesynchroniseerd moet worden uit de lijst van beschikbare synchronisatiegroepen. Kies 'Geen synchronisatie' om deze functie te annuleren. NO Spilleren kan synkroniseres med andre spillere, slik at de spiller samme musikk samtidig. Velg spillerne du ønsker å synkronisere med fra listen over synkroniseringsgrupper. Velg Ingen synkronisering for å stoppe synkroniseringen. PL Odtwarzacz można zsynchronizować z innymi odtwarzaczami, co umożliwia im jednoczesne odtwarzanie muzyki. Wybierz odtwarzacze do zsynchronizowania z listy dostępnych grup synchronizacji. Wybierz opcję Bez synchronizacji, aby zatrzymać synchronizację. PT O cliente por estar sincronizado com outros clientes, permitindo que toquem todos a mesma música em simultâneo. Escolha os clientes que deseja sincronizar na lista de grupos disponíveis para sincronizar. Escolha "Não Sincronizado" para parar a sincronização. @@ -4709,7 +4709,7 @@ SETUP_SYNCVOLUME_ON HE סינכרון עוצמת הקול של הנגן IT Sincronizza il volume dei lettori JA プレーヤーのボリュームをシンクロさせる - NL Volume van muzieksysteem synchroniseren + NL Volume van muziekspeler synchroniseren NO Synkroniser spillerens volum PL Synchronizuj głośność odtwarzacza PT Sincronizar o volume do cliente @@ -4728,7 +4728,7 @@ SETUP_SYNCVOLUME_OFF HE ללא סינכרון עוצמת הקול של הנגן IT Non sincronizzare il volume dei lettori JA プレーヤーのボリュームをシンクロさせない - NL Volume van muzieksysteem niet synchroniseren + NL Volume van muziekspeler niet synchroniseren NO Ikke synkroniser spillerens volum PL Nie synchronizuj głośności odtwarzacza PT Não sincronizar o volume do cliente @@ -4766,7 +4766,7 @@ SETUP_SYNCVOLUME_DESC HE באפשרותך לבחור לגרום לרמות עוצמת הקול של הנגנים המסונכרנים להתאים זו לזו או להיות נפרדות. בחר אחת מהאפשרויות שלהלן ולחץ על 'שנה'. IT È possibile sincronizzare il volume dei lettori. Effettuare la selezione qui sotto e fare clic su Cambia. JA シンクロ時のボリュームをシンクロさせることができます。以下で選択して「変更」をクリックして下さい。 - NL Je kunt de volumeniveaus van gesynchroniseerde muzieksystemen op elkaar afstemmen of deze onafhankelijk instellen. Maak hieronder een selectie en klik op de knop Veranderen. + NL Je kunt de volumeniveaus van gesynchroniseerde muziekspelers op elkaar afstemmen of deze onafhankelijk instellen. Maak hieronder een selectie en klik op de knop Wijzigen. NO Du kan velge å spore lydnivået mellom synkroniserte spillere, eller la lydnivået være en individuell innstilling i hver spiller. Foreta et valg nedenfor, og klikk Endre. PL Poziom głośności zsynchronizowanych odtwarzaczy może być przez nie śledzony lub można ustawić go niezależnie. Wybierz opcję poniżej i kliknij przycisk Zmień. PT Aqui pode activar ou desactivar a sincronização do volume entre os vários clientes. Altere abaixo e clique em "Modificar" @@ -4839,7 +4839,7 @@ SETUP_SYNCPOWER_DESC HE באפשרותך לבחור לגרום לנגן לכבות בעצמו, או לכבות כחלק מקבוצה עם כל יתר הנגנים בקבוצה. בחר אחת מהאפשרויות שלהלן ולחץ על 'שנה'. IT Si può scegliere di spegnere questo lettore in maniera autonoma o di spegnerlo assieme agli altri lettori appartenenti al gruppo. Effettuare la selezione qui sotto e fare clic su Cambia. JA プレーヤーの電源を独立でオフにことも、グループでオフにすることもできます。以下で選択してください。 - NL Je kunt ervoor kiezen om dit muzieksysteem apart uit te zetten, of als deel van een groep met de andere muzieksystemen in die groep. Maak hieronder een selectie en klik op de knop Veranderen. + NL Je kunt ervoor kiezen om deze muziekspeler apart uit te zetten, of als deel van een groep met de andere muziekspelers in die groep. Maak hieronder een selectie en klik op de knop Wijzigen. NO Du kan velge å la denne spilleren slå seg av alene, eller samkjørt med de andre spillerne i gruppen. Velg nedenfor, og trykk «Endre». PL Odtwarzacz ten może wyłączać się samodzielnie lub jako grupa z innymi odtwarzaczami. Wybierz opcję poniżej i kliknij przycisk Zmień. RU Можно выбрать, будет ли данный плеер выключаться отдельно или вместе с другими плеерами данной группы. Выберите нужный вариант и нажмите кнопку "Изменить". @@ -4967,7 +4967,7 @@ SETUP_BUFFERSECS_DESC FR Lors de la lecture d'un flux Internet, la platine place une petite quantité des données dans le tampon avant de commencer la lecture. Vous pouvez définir la taille en secondes (entre 3 et 30) de ce tampon. Si vous faites l'expérience de coupures audio, augmentez cette valeur. HE בעת הפעלה של זרימת אינטרנט, הנגן אוגר כמות קטנה של נתונים לפני תחילת ההשמעה. ציין את כמות נתוני השמע שיש לאגור, במדד של שניות בין 3-30. ערך ברירת המחדל הוא 3 שניות. אם השמע נשמע מקוטע, ייתכן שהגדלת הערך תעזור. IT Quando si riproduce uno stream Internet, il lettore memorizza una piccola quantità di dati prima di iniziare la riproduzione. Specificare la quantità di dati audio da memorizzare, in secondi da 3 a 30. Il valore predefinito è 3 secondi. Se lo stream dell'audio non è costante, l'aumento di questo valore potrebbe determinare un miglioramento. - NL Bij het afspelen van een internetstream plaatst het muzieksysteem een kleine hoeveelheid gegevens in de buffer, voordat het afspelen start. Specificeer in seconden hoeveel audiogegevens moeten worden gebufferd. De standaardwaarde is 3, de maximumwaarde 30 seconden. Als de audio hapert, kan het helpen de buffer te vergroten. + NL Bij het afspelen van een internetstream plaatst de muziekspeler een kleine hoeveelheid gegevens in de buffer, voordat het afspelen start. Specificeer in seconden hoeveel audiogegevens moeten worden gebufferd. De standaardwaarde is 3, de maximumwaarde 30 seconden. Als de audio hapert, kan het helpen de buffer te vergroten. NO Ved avspilling av Internett-strømmer vil spilleren mellomlagre en liten datamengde før den begynner avspillingen. Angi størrelsen på dette mellomlageret (3–30 sekunder). Standardverdien er 3 sekunder. Hvis du opplever at lyden er hakkete, kan det hjelpe å øke denne verdien. PL Podczas odtwarzania strumienia internetowego odtwarzacz buforuje niewielką ilość danych przed rozpoczęciem odtwarzania. Określ ilość danych audio do buforowania w sekundach od 3 do 30. Wartość domyślna to 3 sekundy. Jeżeli dźwięk będzie odtwarzany z przerwami, zwiększenie tej wartości może rozwiązać problem. RU Перед воспроизведением интернет-потока плеер буферизует небольшой объем данных. Укажите размер буферизуемых аудиоданных в секундах, от 3 до 30. Значение по умолчанию — 3 с. Увеличив это значение, можно устранить прерывистость воспроизведения аудио. @@ -5015,7 +5015,7 @@ SETUP_SYNCSTARTDELAY FI Synkronoitujen soitinten käynnistysviive (ms) FR Délai de démarrage des platines synchronisées (ms) IT Ritardo avvio lettori sincronizzati (ms) - NL Vertraging bij opstarten gesynchroniseerde muzieksystemen (ms) + NL Opstartvertraging bij gesynchroniseerde muziekspelers (ms) NO Forsinkelse ved oppstart av synkroniserte spillere (ms) PL Opóźnienie uruchomienia zsynchronizowanych odtwarzaczy (ms) RU Задержка при запуске синхронизованных плееров (мс) @@ -5030,7 +5030,7 @@ SETUP_SYNCSTARTDELAY_DESC FI Jos useita soittimia synkronoidaan, Logitech Media Server käskee kaikkia niistä aloittamaan kappaleen toiston samaan aikaan. Sen täytyy varmistaa, että jokaisella soittimella on riittävästi aikaa aloituskomennon vastaanottamiseen, ennen kuin kaikki soittimet aloittavat toiston yhdessä, joten se lähettää aloituskomennon etuajassa, ja soittimet odottavat oikeaa hetkeä toiston aloittamiseen. Jos verkkoliikenne on ruuhkaista, oletusviive 200 ms ei ole välttämättä riittävä. Näin saattaa tapahtua, jos verkossa on paljon soittimia. Tämä on yleinen ongelma myös langattomissa verkoissa. Voit muuttaa oletusviivettä tarvittaessa. FR Lorsque plusieurs platines sont synchronisées, le Logitech Media Server indique à chacune de démarrer la lecture du morceau en même temps. Le serveur vérifie que chaque platine dispose d'assez de temps pour recevoir la commande avant de démarrer. Il envoie donc la commande en avance et chaque platine attend le bon moment pour démarrer. Si le réseau est surchargé, comme cela peut être le cas si de nombreuses platines sont présentes et avec des réseaux sans fil, le délai de 200 ms par défaut peut ne pas être suffisant. Vous pouvez modifier la valeur par défaut si nécessaire. IT Quando più lettori sono sincronizzati, Logitech Media Server ordina a ciascun lettore di avviare la riproduzione del brano simultaneamente. A questo scopo, deve assicurarsi che ciascun lettore abbia il tempo di ricevere il comando di avvio prima dell'inizio della riproduzione, pertanto invia il comando di avvio con un certo anticipo in modo che ciascun lettore sia pronto a iniziare al momento giusto. Se il traffico di rete è eccessivo, come può succedere se sono connessi numerosi utenti in particolare nelle reti senza fili, il ritardo predefinito di 200 ms potrebbe non essere sufficiente. Se necessario, è possibile modificare questo valore predefinito. - NL Wanneer meerdere muzieksystemen worden gesynchroniseerd, geeft Logitech Media Server elk systeem de opdracht het nummer op hetzelfde moment te starten. Logitech Media Server moet ervoor zorgen dat elk systeem genoeg tijd heeft om de startopdracht te ontvangen voordat alle systemen samen starten, en daarom wordt de startopdracht van tevoren verzonden en wacht elk systeem tot het juiste moment om te starten. Als het netwerkverkeer overbelast is, wat voornamelijk kan gebeuren bij netwerken met vele systemen of bij draadloze netwerken, is de standaardvertraging van 200 milliseconden misschien niet voldoende. Je kunt de standaardvertraging hier desgewenst wijzigen. + NL Wanneer meerdere muziekspelers worden gesynchroniseerd, geeft Logitech Media Server elk systeem de opdracht het nummer op hetzelfde moment te starten. Logitech Media Server moet ervoor zorgen dat elk systeem genoeg tijd heeft om de startopdracht te ontvangen voordat alle systemen samen starten, en daarom wordt de startopdracht van tevoren verzonden en wacht elk systeem tot het juiste moment om te starten. Als het netwerkverkeer overbelast is, wat voornamelijk kan gebeuren bij netwerken met vele systemen of bij draadloze netwerken, is de standaardvertraging van 200 milliseconden misschien niet voldoende. Je kunt de standaardvertraging hier desgewenst wijzigen. NO Når flere spillere er synkronisert, sender Logitech Media Server beskjed til hver spiller om å begynne avspilling samtidig. Den må sikre at hver spiller får tid til å motta kommandoen før de alle starter samtidig, så den sender derfor startkommandoen på forhånd, og så venter spillerne til riktig øyeblikk før de starter. Hvis det er mye nettverkstrafikk, noe som kan skje hvis det er mange spillere på nettverket og ved bruk av trådløse nettverk, kan det hende at standardforsinkelsen på 200 ms er for kort. Hvis nødvendig kan du endre standardverdien her. PL W przypadku zsynchronizowania kilku odtwarzaczy program Logitech Media Server wysyła do każdego odtwarzacza polecenie rozpoczęcia odtwarzania utworu w tym samym czasie. Program musi sprawdzić, czy każdy odtwarzacz ma czas na odebranie polecenia rozpoczęcia przed rozpoczęciem jednoczesnego odtwarzania, więc wysyła wcześniej polecenie rozpoczęcia, a odtwarzacze rozpoczynają pracę w odpowiedniej chwili. W przypadku nadmiernego przeciążenia sieci, które występuje szczególnie wtedy, gdy w sieciach standardowych i bezprzewodowych używana jest duża liczba odtwarzaczy, domyślne opóźnienie o długości 200 ms może być niewystarczające. W razie potrzeby wartość domyślną można zmienić tutaj. RU При синхронизации нескольких плееров Logitech Media Server дает каждому из них команду начать воспроизведение дорожки в одно и то же время. Для этого необходимо убедиться, что каждый плеер получит команду запуска до всеобщего начала воспроизведения, поэтому данная команда отправляется ему заранее. После получения команды каждый плеер ожидает запуска. При сильной загрузке сети, что особенно характерно для сетей со множеством плееров и для беспроводных сетей, задержка по умолчанию, равная 200 мс, может быть недостаточной. При необходимости это значение можно изменить. @@ -5045,7 +5045,7 @@ SETUP_STARTDELAY FI Soittimen käynnistysviive (ms) FR Délai de démarrage de la platine (ms) IT Ritardo avvio lettore (ms) - NL Vertraging bij opstarten muzieksysteem (ms) + NL Startvertraging van muziekspeler (ms) NO Forsinket oppstart av spiller (ms) PL Opóźnienie uruchomienia odtwarzacza (ms) RU Задержка запуска плеера (мс) @@ -5060,7 +5060,7 @@ SETUP_STARTDELAY_DESC FI Saattaa kulua huomattava aika, ennen kuin soitin aloittaa toiston (ennen kuin ääni alkaa kuulua), sen jälkeen, kun Logitech Media Server on antanut aloittamiskomennon. Näin saattaa käydä, jos käytetään digitaalilähtöjä (riippuen yhdistetyistä laitteista). Joissakin käyttöjärjestelmissä esiintyy myös ongelmia soitinohjelmistojen kanssa. Tällaisessa tapauksessa viive voidaan määrittää tässä, jotta Logitech Media Server tietää, kuinka paljon etukäteen soitin pitää käynnistää, jotta ääni alkaa kuulua samanaikaisesti kaikista synkronoiduista soittimista. Huomaa: tämä viive on lisäys soittimen äänen normaaliin viiveeseen (alla). FR Il est possible qu'une platine mette un certain temps à démarrer (avant d'entendre le morceau lu) après avoir reçu la commande de démarrage du serveur. Tel peut être le cas avec des sorties numériques (en fonction de l'équipement connecté) ou avec des lecteurs logiciels sur certaines plates-formes. Dans de tels cas, il est possible de régler le délai pour que le Logitech Media Server connaisse le délai en fonction duquel il doit démarrer cette platine en avance afin que l'audio de toutes les platines synchronisées démarre en même temps. Remarque: cette valeur est ajoutée à celle définie pour Délai audio de la platine (ci-dessous). IT L'avvio della riproduzione di un lettore può richiedere un intervallo considerevole (prima che venga emesso l'audio) dopo l'elaborazione del comando di avvio in Logitech Media Server. Un caso del genere può verificarsi quando si utilizzano ingressi digitali (a seconda dell'apparecchiatura collegata) o con lettori software di alcune piattaforme. In questi casi il ritardo qui impostato consente di definire con quanto anticipo Logitech Media Server deve avviare il lettore per consentire l'inizio simultaneo della riproduzione in tutti i lettori sincronizzati. Nota: questo ritardo si aggiunge a quello impostato in "Ritardo audio lettore" (sotto). - NL Nadat een muzieksysteem de startopdracht van Logitech Media Server heeft verwerkt, kan het een tijdje duren voordat het systeem begint met afspelen (voordat je de audio hoort). Dit kan het geval zijn bij gebruik van digitale uitgangen (afhankelijk van de aangesloten apparatuur) of bij softwarespelers op sommige platforms. Deze vertraging kan dan hier worden ingesteld, zodat Logitech Media Server weet hoever van tevoren dit systeem moet worden gestart om de audio uit alle gesynchroniseerde systemen tegelijk te laten beginnen. N.B. Deze optie kan ook ingesteld worden terwijl Audiovertraging muzieksysteem (hieronder) actief is. + NL Nadat een muziekspeler de startopdracht van Logitech Media Server heeft verwerkt, kan het een tijdje duren voordat het systeem begint met afspelen (voordat je de audio hoort). Dit kan het geval zijn bij gebruik van digitale uitgangen (afhankelijk van de aangesloten apparatuur) of bij softwarespelers op sommige platforms. Deze vertraging kan dan hier worden ingesteld, zodat Logitech Media Server weet hoever van tevoren dit systeem moet worden gestart om de audio uit alle gesynchroniseerde systemen tegelijk te laten beginnen. N.B. Deze optie kan ook ingesteld worden terwijl Audiovertraging muziekspeler (hieronder) actief is. NO Det kan ta noe tid før spilleren begynner avspillingen (dvs. før du hører lyden) etter at den har prosessert startkommandoen fra Logitech Media Server. Dette kan skje når du bruker digitale utganger (avhengig av utstyret som er koplet til), eller med spillerprogrammer på enkelte plattformer. Hvis dette skjer, kan du legge inn denne forsinkelsen her, slik at Logitech Media Server vet hvor mye tidligere den må starte denne spilleren for å synkronisere lyden fra alle synkroniserte spillere. Merk: dette kommer i tillegg til eventuell «Lydforsinkelse for spiller» (nedenfor). PL Rozpoczęcie odtwarzania (zanim usłyszysz dźwięk) po przetworzeniu przez odtwarzacz polecenia uruchomienia z programu Logitech Media Server może być zauważalnie opóźnione. Może to mieć miejsce w przypadku korzystania z wyjść cyfrowych (w zależności od podłączonych urządzeń) lub odtwarzaczy programowych na niektórych platformach. W takich przypadkach opóźnienie to można ustawić tutaj, aby program Logitech Media Server uruchomił odtwarzacz odpowiednio wcześniej w celu jednoczesnego uruchomienia wszystkich zsynchronizowanych odtwarzaczy. Uwaga: opcja ta stanowi dodatek dla ustawienia „Opóźnienie dźwięku odtwarzacza” (poniżej). RU После обработки команды запуска Logitech Media Server плеер может начать воспроизведение лишь через некоторое время (до того, как появится звук). Обычно это происходит при работе с цифровыми выходами (в зависимости от подключенного оборудования) или с программными плеерами на некоторых платформах. В этих случаях можно настроить длительность задержки таким образом, чтобы Logitech Media Server запускал данный плеер заблаговременно и звук со всех синхронизованных плееров появлялся в одно и то же время. Примечание: этот параметр дополняет любое из текущих значений "Задержка звука плеера" (см. ниже). @@ -5090,7 +5090,7 @@ SETUP_MAINTAINSYNC_DESC FI Vaikka useat soittimet aloittaisivatkin kappaleen soiton täysin synkronoidusti, ajan myötä (yleensä muutaman minuutin kuluttua) niiden välille syntyy eroja. Yli 30 millisekunnin ero on havaittava, jos kuulet musiikin useista lähteistä samanaikaisesti. Logitech Media Server pyrkii korjaamaan virheen seuraamalla jatkuvasti sitä, kuinka pitkään kukin soitin on soittanut, ja tekemällä pieniä säätöjä tarpeen mukaan. Voit valita, pidätkö toiminnon käytössä. FR Même lorsque plusieurs platines commencent à lire un morceau en parfaite synchronisation, il est possible qu'elles se désynchronisent au fil du temps (après quelques minutes, en général). Une différence de plus de 30ms sera perceptible si vous pouvez entendre le son en provenance de ces platines. Le Logitech Media Server effectue le suivi des morceaux lus par chaque platine et effectue les ajustements nécessaires. Vous pouvez activer ou désactiver ce comportement. IT Anche se un brano viene inizialmente riprodotto con più lettori in perfetta sincronia, è possibile che nell'arco di alcuni minuti la riproduzione non sia più sincronizzata. Se si ha la possibilità di ascoltare un brano con più lettori contemporaneamente, sarà percepibile una differenza superiore a 30 ms (millisecondi). Logitech Media Server risolve questo problema tenendo costantemente traccia della durata di brano riprodotta e apportando le eventuali regolazioni necessarie. È possibile attivare o disattivare questa funzione. - NL Zelfs wanneer verschillende muzieksystemen een nummer vanaf het begin perfect gesynchroniseerd afspelen, kunnen ze op een gegeven moment van elkaar afwijken (meestal na een paar minuten). Een verschil van meer dan 30 milliseconden is merkbaar als je de uitvoer van meerdere systemen tegelijk kunt horen. Logitech Media Server lost dit op door voortdurend bij te houden hoeveel elk systeem heeft afgespeeld en waar nodig kleine aanpassingen te maken. Je kunt dit gedrag in- of uitschakelen. + NL Zelfs wanneer verschillende muziekspelers een nummer vanaf het begin perfect gesynchroniseerd afspelen, kunnen ze op een gegeven moment van elkaar afwijken (meestal na een paar minuten). Een verschil van meer dan 30 milliseconden is merkbaar als je de uitvoer van meerdere systemen tegelijk kunt horen. Logitech Media Server lost dit op door voortdurend bij te houden hoeveel elk systeem heeft afgespeeld en waar nodig kleine aanpassingen te maken. Je kunt dit gedrag in- of uitschakelen. NO Selv når flere spillere spiller samme spor og er perfekt synkronisert, kan de gli fra hverandre over tid (vanligvis etter noen få minutter). Hvis forskjellen mellom to avspillinger er på mer enn rundt 30 millisekunder, vil du kunne merke det hvis du hører lyden fra flere spillere samtidig. Logitech Media Server håndterer dette ved å spore hvor mye hver spiller til enhver tid har spilt av, og ved å gjøre små endringer etter behov. Du kan slå denne justeringen av og på. PL Nawet jeżeli kilka odtwarzaczy rozpocznie odtwarzanie utworu dokładnie w tym samym momencie, po pewnym czasie (zwykle po kilku minutach) nie będą już zsynchronizowane. Różnica większa niż 30 ms (milisekund) będzie zauważana w przypadku słuchania dźwięku z kilku odtwarzaczy jednocześnie. Program Logitech Media Server rozwiązuje ten problem przez stałe śledzenie, jaka część utworu została odtworzona przez dany odtwarzacz i wprowadzanie niewielkich zmian w razie potrzeby. Funkcję tę można włączyć lub wyłączyć. RU Даже если несколько плееров начинают воспроизводить дорожку абсолютно синхронно, со временем (обычно через несколько минут) могут появиться расхождения. Расхождения более 30 мс будут заметны, если одновременно слушать музыку из нескольких плееров. Logitech Media Server решает эту проблему, отслеживая время работы каждого плеера и корректируя ход воспроизведения. Эту функцию можно включать и отключать. @@ -5135,7 +5135,7 @@ SETUP_PLAYDELAY FI Soittimen ääniviive (ms) FR Délai audio de la platine (ms) IT Ritardo audio lettore (ms) - NL Audiovertraging muzieksysteem (ms) + NL Audiovertraging muziekspeler (ms) NO Lydforsinkelse for spiller (ms) PL Opóźnienie dźwięku odtwarzacza (ms) RU Задержка звука плеера (мс) @@ -5150,7 +5150,7 @@ SETUP_PLAYDELAY_DESC FI Saattaa olla huomattavia eroja sen välillä, kuinka suuren osan kappaleesta soitin ilmoittaa soittaneensa Logitech Media Serverille, ja sen, kuinka suuri osa kappaleesta on kuunneltu. Näin saattaa käydä, jos käytetään digitaalilähtöjä (riippuen yhdistetyistä laitteista). Joissakin käyttöjärjestelmissä esiintyy myös ongelmia soitinohjelmistojen kanssa. Tällaisessa tapauksessa viive voidaan määrittää tässä kohdassa, niin että Logitech Media Server voi ottaa sen huomioon synkronointia säätäessään. FR Il peut y avoir une différence entre la durée de lecture d'un morceau écoulée indiquée par la platine et la quantité du morceau qui a effectivement été lue. Tel peut être le cas avec des sorties numériques (en fonction de l'équipement connecté) ou avec des lecteurs logiciels sur certaines plates-formes. Dans de tels cas, ce délai peut être défini ici de manière à ce que le Logitech Media Server le prenne en compte lors des ajustements de synchronisation. IT Potrebbe sussistere una differenza notevole tra l'audio segnalato come riprodotto in Logitech Media Server con il lettore e quello effettivamente ascoltato. Un caso del genere può verificarsi quando si utilizzano ingressi digitali (a seconda dell'apparecchiatura collegata) o con lettori software di alcune piattaforme. In questo caso è possibile impostare qui un ritardo che Logitech Media Server terrà in considerazione durante la regolazione della sincronizzazione. - NL Er kan een merkbaar verschil zijn tussen hoeveel van een door het muzieksysteem gerapporteerd nummer naar Logitech Media Server is afgespeeld en hoeveel audio is gehoord. Dit kan het geval zijn bij gebruik van digitale uitgangen (afhankelijk van de aangesloten apparatuur) of bij softwaresystemen op sommige platforms. Deze vertraging kan dan hier worden ingesteld, zodat Logitech Media Server er rekening mee kan houden bij het aanpassen van de synchronisatie. + NL Er kan een merkbaar verschil zijn tussen hoeveel van een door de muziekspeler gerapporteerd nummer naar Logitech Media Server is afgespeeld en hoeveel audio is gehoord. Dit kan het geval zijn bij gebruik van digitale uitgangen (afhankelijk van de aangesloten apparatuur) of bij softwaresystemen op sommige platforms. Deze vertraging kan dan hier worden ingesteld, zodat Logitech Media Server er rekening mee kan houden bij het aanpassen van de synchronisatie. NO Det kan være markant forskjell på hvor mye av et spor som har blitt spilt av ifølge Logitech Media Server, og hvor mye lyd som faktisk har kommet ut. Dette kan skje når du bruker digitale utganger (avhengig av utstyret som er koplet til), eller med spillerprogrammer på enkelte plattformer. Hvis dette skjer, kan du legge inn denne forsinkelsen her, slik at Logitech Media Server tar den med i beregningen når den justerer synkroniseringen. PL Różnica między czasem odtworzenia utworu przesłanym przez odtwarzacz do programu Logitech Media Server, a odtworzonym dźwiękiem może być znaczna. Może to mieć miejsce w przypadku korzystania z wyjść cyfrowych (w zależności od podłączonych urządzeń) lub odtwarzaczy programowych na niektórych platformach. W takich przypadkach opóźnienie to można ustawić tutaj, aby program Logitech Media Server uwzględniał je podczas dostosowywania synchronizacji. RU Существует значительная разница между отчетом плеера о воспроизведенной части дорожки Logitech Media Server и фактической продолжительностью прослушанного аудио. Это бывает при использовании цифровых выходов (в зависимости от подключенного оборудования) или программных плееров на некоторых платформах. Для таких случаев здесь можно задать величину задержки, учитываемую Logitech Media Server при настройке синхронизации. @@ -5180,7 +5180,7 @@ SETUP_MINSYNCADJUST_DESC FI Jos Logitech Media Server havaitsee, että kaksi synkronoitua soitinta on joutunut hieman epätahtiin toistensa kanssa, se tekee pienen säädön, yleensä soittimeen, joka on jäänyt jälkeen. Voit määrittää tällaisen säädön vähimmäiskoon tälle soittimelle. Mitä suurempi intervalli on, sitä harvemmin säätöjä tehdään, mutta sitä useammin soittimet myös joutuvat epätahtiin. FR Lorsque le Logitech Media Server détecte que deux platines se sont désynchronisées, il effectue des ajustements, généralement sur la platine qui a pris du retard. Vous pouvez configurer la taille minimale de ces ajustements pour cette platine. Plus l'intervalle est important, plus les ajustements seront espacés. Les retards autorisés pour les platines seront ainsi plus importants. IT Quando Logitech Media Server rileva che due lettori non sono più perfettamente sincronizzati, nella maggior parte dei casi apporta una lieve regolazione al lettore più lento. È possibile configurare le dimensioni minime di tali regolazioni nel lettore corrente. Intervalli maggiori riducono la frequenza delle regolazioni e comportano una maggiore differenza di sincronizzazione. - NL Wanneer Logitech Media Server detecteert dat twee muzieksystemen niet langer helemaal synchroon lopen, wordt er een kleine aanpassing gemaakt, meestal op het systeem dat achterloopt. Je kunt de minimumlengte van dergelijke aanpassingen voor dit systeem configureren. Hoe groter het interval, des te minder vaak er aanpassingen worden gemaakt en hoe groter de afwijking tussen de twee systemen wordt. + NL Wanneer Logitech Media Server detecteert dat twee muziekspelers niet langer helemaal synchroon lopen, wordt er een kleine aanpassing gemaakt, meestal op het systeem dat achterloopt. Je kunt de minimumlengte van dergelijke aanpassingen voor dit systeem configureren. Hoe groter het interval, des te minder vaak er aanpassingen worden gemaakt en hoe groter de afwijking tussen de twee systemen wordt. NO Når Logitech Media Server finner ut at to synkroniserte spillere ikke er helt i synk, utfører den en liten justering, vanligvis hos spilleren som er i bakleksa. Du kan konfigurere minstestørrelsen på slike justeringer for denne spilleren. Jo større intervallet er, jo mindre hyppige blir justeringene, men jo mer kan spillerne komme i utakt. PL Gdy program Logitech Media Server wykryje, że dwa zsynchronizowane odtwarzacze są nieznacznie rozsynchronizowane, dokona niewielkich zmian, zwykle dotyczących odtwarzacza, w którym występuje opóźnienie. Możliwe jest skonfigurowanie minimalnego dostosowania dla tego odtwarzacza. Im większy jest interwał, tym rzadziej nastąpi zmiana, ale więcej odtwarzaczy nie będzie zsynchronizowanych. RU Когда Logitech Media Server обнаруживает некоторое отклонение в синхронизации двух плееров, проводится небольшая регулировка, обычно по отношению к отставшему плееру. Можно настроить минимальный размер регулировки данного плеера. Чем больше заданный интервал, тем реже выполняется регулировка, но тем больше размер возможного отклонения синхронизованных плееров. @@ -5210,7 +5210,7 @@ SETUP_PACKETLATENCY_DESC FI Logitech Media Server mittaa automaattisesti verkkoviiveen sen ja soittimen välillä. Tämä on tärkeää synkronoinnin ylläpitämistä varten. Toisinaan mittaukset saattava olla liian ristiriitaisia, jotta niistä olisi hyötyä. Tällaisessa tapauksessa voit määrittää tyypillisen arvon. FR Le Logitech Media Server mesure automatiquement la latence du réseau entre le serveur et la platine. Ces données sont importantes pour conserver la synchronisation entre les deux. Il arrive que ces mesures soient incohérentes, auquel cas vous devrez spécifier une valeur ici. IT Logitech Media Server calcola automaticamente la latenza di rete rispetto al lettore. Questo dato è fondamentale per mantenere la sincronizzazione. In alcuni casi questi calcoli producono risultati troppo disomogenei per poter essere utilizzati. Pertanto è possibile specificare un valore indicativo in questo punto. - NL Logitech Media Server meet automatisch de netwerkvertraging tussen zichzelf en het muzieksysteem. Dit is belangrijk voor het handhaven van synchronisatie. Af en toe zijn deze metingen te inconsistent om gebruikt te kunnen worden. In dat geval kun je hier een standaardwaarde opgeven. + NL Logitech Media Server meet automatisch de netwerkvertraging tussen zichzelf en de muziekspeler. Dit is belangrijk voor het handhaven van synchronisatie. Af en toe zijn deze metingen te inconsistent om gebruikt te kunnen worden. In dat geval kun je hier een standaardwaarde opgeven. NO Logitech Media Server måler automatisk nettverksforsinkelsen mellom seg selv og spilleren. Dette er viktig for å opprettholde synkronisering. Disse målingene kan iblant være for variable til at de kan brukes. Da kan du i stedet angi en typisk verdi her. PL Program Logitech Media Server automatycznie mierzy opóźnienie sieci między nim a odtwarzaczem. Jest to ważne w celu zachowania synchronizacji. Czasami te pomiary mogą być bardzo niespójne, a przez to nieprzydatne. W takim przypadku można określić typową wartość w tym miejscu. RU Logitech Media Server автоматически измеряет задержку в сети между ним и плеером. Это важно для сохранения синхронизации. Иногда эти измерения могут слишком отличаться друг от друга, чтобы выявить какую-то закономерность: в этом случае можно ввести типичное значение в данное поле. @@ -5245,7 +5245,7 @@ SETUP_MAXBITRATE_DESC HE Logitech Media Server יכול להמיר קובצי מוסיקה לתבנית WAV/AIFF או MP3 באופן אוטומטי במהלך השמעת שירים כדי להוריד את קצב הסיביות של הנתונים המוזרמים לנגן. הגדרה זו שימושית אם אתה מחבר את הנגן ל-Logitech Media Server דרך האינטרנט או נתקל בבעיות של קיטועים בשימוש ברשת אלחוטית. באפשרותך לבחור את קצב הסיביות המרבי שבו Logitech Media Server יפעיל זרימות לנגן. שירים בתבנית MP3 שקודדו בקצב נתונים נמוך מזה המצוין להלן, לא יומרו. אם תבחר את האפשרות 'ללא הגבלה', Logitech Media Server יפעיל זרימות של קובצי MP3 מבלי להמירם ויפעיל את יתר סוגי הקבצים כשמע PCM לא דחוס. IT In Logitech Media Server è possibile eseguire rapidamente la conversione automatica dei file musicali WAV/AIFF o MP3 in modo da ridurre il bitrate dei dati inviati al lettore. Questa operazione può essere utile se si collega il lettore a un server tramite Internet o se si verificano interruzioni saltuarie con una rete senza fili. È possibile impostare il bitrate massimo con cui inviare lo stream audio al lettore. I brani MP3 codificati a un bitrate inferiore a quello massimo non vengono convertiti. Se si sceglie Nessun limite, il server trasferisce i file MP3 senza convertirli e gli altri tipi di file come audio PCM non compresso. JA Logitech Media Serverは、自動的にあなたの音楽ファイルをWAV/AIFFまたはMP3に変換します。ビットレートの最大値を設定することができます。「制限なし」を選択しますと、元のファイル通りのビットレートで再生されます。 - NL Logitech Media Server kan je muziekbestanden direct automatisch naar WAV/AIFF of MP3 converteren om gegevens met een lagere bitsnelheid naar je muzieksysteem te streamen. Dit is handig als je systeem via het internet met Logitech Media Server verbonden is, of als je af en toe problemen hebt met een draadloos netwerk. Je kunt kiezen met welke bitsnelheid Logitech Media Server maximaal naar je systeem zal streamen. MP3-nummers die gecodeerd zijn met een lagere bitsnelheid dan de maximale bitsnelheid, worden niet geconverteerd. Als je 'Geen limiet' kiest, streamt Logitech Media Server MP3-bestanden zonder conversie en andere bestandstypen als niet-gecomprimeerde PCM-audio. + NL Logitech Media Server kan je muziekbestanden direct automatisch naar WAV/AIFF of MP3 converteren om gegevens met een lagere bitsnelheid naar je muziekspeler te streamen. Dit is handig als je systeem via het internet met Logitech Media Server verbonden is, of als je af en toe problemen hebt met een draadloos netwerk. Je kunt kiezen met welke bitsnelheid Logitech Media Server maximaal naar je systeem zal streamen. MP3-nummers die gecodeerd zijn met een lagere bitsnelheid dan de maximale bitsnelheid, worden niet geconverteerd. Als je 'Geen limiet' kiest, streamt Logitech Media Server MP3-bestanden zonder conversie en andere bestandstypen als niet-gecomprimeerde PCM-audio. NO Logitech Media Server kan automatisk konvertere musikkfilene til wav/aiff eller mp3 under avspilling, for å begrense bithastigheten på datastrømmen til spilleren. Dette kan være nyttig hvis du kopler spilleren til serveren over Internett, eller hvis du har periodevise problemer med et trådløst nettverk. Du kan angi maksimal bithastighet for strømmingen til spilleren fra Logitech Media Server. Mp3-filer som er kodet med en lavere bithastighet enn maksimumhastigheten, blir ikke konvertert. Hvis du velger Ubegrenset, strømmer serveren mp3-filer uten konvertering, og andre filtyper som ukomprimert pcm-lyd. PL Program Logitech Media Server może automatycznie przekonwertować w locie pliki WAV/AIFF lub MP3 na niższą szybkość transmisji bitów danych przesyłanych strumieniowo do odtwarzacza. Jest to przydatne w przypadku łączenia odtwarzacza z programem Logitech Media Server przez Internet lub tymczasowych problemów z siecią bezprzewodową. Możliwe jest także wybranie maksymalnej szybkości transmisji bitów danych przesyłanych strumieniowo przez program Logitech Media Server do odtwarzacza. Utwory w formacie MP3 zakodowane z szybkością transmisji bitów niższą niż podana poniżej nie zostaną przekonwertowane. Wybranie opcji Bez ograniczeń spowoduje, że program Logitech Media Server będzie strumieniowo przesyłał pliki MP3 bez konwersji, a pliki innych typów jako nieskompresowany dźwięk PCM. RU Logitech Media Server может автоматически преобразовывать музыкальные файлы в формат WAV/AIFF или MP3 в режиме реального времени, чтобы уменьшить скорость передачи данных на плеер. Это удобно при подключении плеера к Logitech Media Server через Интернет или при неустойчивой работе беспроводной сети. Можно задать максимальную скорость передачи со Logitech Media Server на плеер. MP3-файлы со скоростью передачи ниже указанного значения преобразованы не будут. Если выбрать параметр "Без ограничений", Logitech Media Server будет передавать MP3-файлы без преобразования, а другие типы файлов — в виде PCM-аудио без сжатия. @@ -5263,7 +5263,7 @@ SETUP_MP3BITRATE_DESC HE Logitech Media Server יכול להמיר את קובצי המוסיקה שלך באופן אוטומטי ל-MP3 במהלך השמעה של תבניות שאינן MP3. Logitech Media Server גם יכול לדחוס את קובצי ה-MP3 כדי לצמצם את קצב הסיביות של הנתונים המוזרמים לנגן. הגדרה זו שימושית אם אתה מחבר את הנגן ל-Logitech Media Server דרך האינטרנט או נתקל בבעיות של קיטועים בשימוש ברשת אלחוטית. באפשרותך לבחור את קצב הסיביות המרבי שבו Logitech Media Server יפעיל זרימות לנגן. שירים בתבנית MP3 שקודדו בקצב נתונים נמוך מזה המצוין להלן, לא יומרו. IT Logitech Media Server consente di convertire automaticamente i file musicali in MP3 in modo rapido per riprodurre formati diversi da MP3. Consente inoltre di ricomprimere i file MP3 per ridurre il bitrate dei dati inviati al lettore. Questa funzione risulta utile se si connette il lettore a un server tramite Internet o se si verificano occasionali interruzioni con una rete senza fili. È possibile scegliere il bitrate massimo con cui inviare lo stream audio al lettore tramite Logitech Media Server. I brani MP3 codificati a un bitrate inferiore a quello massimo non vengono convertiti. JA Logitech Media Serverは、自動的にあなたの音楽ファイルをMP3に変換します。MP3からさらに下のビットレートへの変換もできます。 - NL Logitech Media Server kan je muziekbestanden direct automatisch naar MP3 converteren om andere formaten dan MP3 af te spelen. Logitech Media Server kan je MP3-bestanden ook opnieuw comprimeren om gegevens met een lagere bitsnelheid naar je muzieksysteem te streamen. Dit is handig als het systeem via het internet met Logitech Media Server verbonden is, of als je af en toe problemen hebt met een draadloos netwerk. Je kunt kiezen met welke bitsnelheid Logitech Media Server maximaal naar je systeem zal streamen. MP3-nummers die gecodeerd zijn met een lagere bitsnelheid dan de maximale bitsnelheid, worden niet geconverteerd. + NL Logitech Media Server kan je muziekbestanden direct automatisch naar MP3 converteren om andere formaten dan MP3 af te spelen. Logitech Media Server kan je MP3-bestanden ook opnieuw comprimeren om gegevens met een lagere bitsnelheid naar je muziekspeler te streamen. Dit is handig als het systeem via het internet met Logitech Media Server verbonden is, of als je af en toe problemen hebt met een draadloos netwerk. Je kunt kiezen met welke bitsnelheid Logitech Media Server maximaal naar je systeem zal streamen. MP3-nummers die gecodeerd zijn met een lagere bitsnelheid dan de maximale bitsnelheid, worden niet geconverteerd. NO Logitech Media Server kan automatisk konvertere musikkfiler til mp3 under avspilling, slik at du kan spille av andre formater enn mp3. Logitech Media Server kan også komprimere mp3-filene dine, for å senke bithastigheten for data som strømmes til spilleren. Dette kan være nyttig hvis du kopler spilleren til serveren over Internett, eller hvis du har periodevise problemer med et trådløst nettverk. Du kan angi maksimal bithastighet for strømmingen til spilleren fra Logitech Media Server. Mp3-filer som er kodet med en bithastighet som er lavere enn maksimumshastigheten, blir ikke konvertert. PL Program Logitech Media Server może automatycznie przekonwertować w locie pliki muzyczne na format MP3 w celu odtworzenia formatów innych niż MP3. Program Logitech Media Server może także ponownie skompresować pliki MP3 na niższą szybkość transmisji bitów danych przesyłanych strumieniowo do odtwarzacza. Jest to przydatne w przypadku łączenia odtwarzacza z programem Logitech Media Server przez Internet lub tymczasowych problemów z siecią bezprzewodową. Możliwe jest także wybranie maksymalnej szybkości transmisji bitów danych przesyłanych strumieniowo przez program Logitech Media Server do odtwarzacza. Utwory w formacie MP3 zakodowane z szybkością transmisji bitów niższą niż podana poniżej nie zostaną przekonwertowane. RU Logitech Media Server может автоматически преобразовывать в формат MP3 музыкальные файлы других форматов в режиме реального времени и воспроизводить их. Logitech Media Server может также повторно сжимать MP3-файлы, чтобы снизить скорость передачи данных на плеер. Это удобно при подключении плеера к Logitech Media Server через Интернет или при неустойчивой работе беспроводной сети. Можно задать максимальную скорость передачи со Logitech Media Server на плеер. MP3-файлы со скоростью передачи ниже указанного значения преобразованы не будут. @@ -5421,7 +5421,7 @@ SETUP_MP3SILENCEPRELUDE_DESC HE למקלטי שמע וממירי D/A מסוימים נחוץ מעט זמן נוסף כדי להתחיל לפענח שמע מהנגן במהלך השמעה של שמע בתבנית MP3. באפשרותך לציין כמה זמן, בשניות, יש להמתין לאחר לחיצה על לחצן ההפעלה כדי להתחיל לשלוח שמע בתבנית MP3. ערך ברירת המחדל הוא 0, ערך של 0.25 שניות עובד עבור מרבית המקלטים. IT Alcuni ricevitori e convertitori audio D/A richiedono un breve intervallo prima di iniziare a decodificare l'audio del lettore per la riproduzione di MP3. È possibile specificare in secondi il tempo che si desidera fare intercorrere tra il momento in cui si preme PLAY e l'inizio dell'invio di audio MP3. Il valore predefinito è 0; un valore pari a 0,25 secondi è indicato per molti ricevitori. JA いくつかのレシーバーやD/Aコンバーターは、MP3を再生する際にデジタル信号をデコードし始めるのに時間がかかることがあります。どれくらいのスタートアップタイムが必要か、秒数で入力してください。標準は0になっています。0.25秒あればほとんどの機器で対応可能です。 - NL Wanneer er MP3-audio afgespeeld wordt, hebben sommige audio-ontvangers en D/A-omzetters wat extra tijd nodig om het decoderen van audio van het muzieksysteem te starten. Je kunt opgeven, in seconden, hoe lang er moet worden gewacht nadat het afspelen is gestart, voordat er MP3-audio wordt verzonden. De standaardwaarde is 0; de waarde 0,25 werkt voor vele muzieksystemen. + NL Wanneer er MP3-audio afgespeeld wordt, hebben sommige audio-ontvangers en D/A-omzetters wat extra tijd nodig om het decoderen van audio van de muziekspeler te starten. Je kunt opgeven, in seconden, hoe lang er moet worden gewacht nadat het afspelen is gestart, voordat er MP3-audio wordt verzonden. De standaardwaarde is 0; de waarde 0,25 werkt voor vele muziekspelers. NO Enkelte forsterkere og D/A-omformere trenger litt ekstra tid for å starte dekoding av mp3-lyd. Du kan angi hvor mange sekunder det skal gå fra du trykker «Play» til mp3-lyden begynner å sendes. Standardverdien er 0. Verdien 0,25 sekunder fungerer bra for mange mottakere. PL Niektóre odbiorniki audio i konwertery cyfrowo-analogowe wymagają krótkiego dodatkowego czasu na rozpoczęcie dekodowania dźwięku z odtwarzacza w przypadku odtwarzania plików MP3. Możliwe jest określenie czasu oczekiwania po naciśnięciu przycisku odtwarzania przed rozpoczęciem wysyłania dźwięku w formacie MP3. Wartość domyślna wynosi 0, a wartość 0,25 sekundy jest odpowiednia dla wielu odbiorników. RU Некоторым приемникам и цифроаналоговым преобразователям (ЦАП) требуется определенное время, чтобы начать перекодирование аудио с плеера при воспроизведении аудио в формате MP3. Можно указать время ожидания (в секундах), которое должно пройти с момента нажатия клавиши воспроизведения до передачи аудио в формате MP3. Значение по умолчанию — 0, для многих плееров рекомендуется задавать значение 0,25 с. @@ -5455,7 +5455,7 @@ SETUP_REPLAYGAINMODE_DESC FR Les fichiers audio peuvent contenir des informations d'ajustement du volume ou gain, appelées Replay Gain, permettant de normaliser le volume apparent d'un morceau ou d'un album durant la lecture. La Squeezebox peut utiliser ces informations lorsqu'elles sont présentes. Le gain morceau permet de normaliser le volume de chaque morceau. Avec le gain album, le volume de chaque album dans son ensemble est normalisé, mais les morceaux d'un même album conserveront leurs différences de volume. Le gain intelligent permet d'utiliser le gain album si des morceaux consécutifs sont issus d'un même album. Sinon, le gain morceau est utilisé. HE רצועות שמע מסוימות מכילה מידע אודות כוונונים של עוצמת הקול, או "הגברה להשמעה חוזרת", שניתן להשתמש בו במהלך השמעה כדי לוודא שעוצמת השמע של הרצועות והאלבומים זהה. הנגן יכול להשתמש במידע זה, אם קיים. ניתן להשתמש ב"הגברת רצועות" כדי לוודא שעוצמת הקול של כל הרצועות זהה. ניתן להשתמש ב"הגברת אלבום" כדי לוודא שעוצמת הקול של כל האלבומים זהה, אך שנשמרים הבדלי עוצמת הקול בין רצועות בכל אלבום. בחירת הגברה "חכמה" משתמשת בהגברת אלבום אם שירים עוקבים הם מאותו אלבום, או בהגברת רצועות עבור רשימת ההשמעה מעורבת. IT Alcune tracce audio contengono una correzione del volume, detta replay gain, che può essere usata durante la riproduzione per assicurarsi che le tracce e gli album abbiano lo stesso livello sonoro. Il lettore può usare questa informazione se presente. Il guadagno traccia può essere usato per assicurarsi che tutte le tracce abbiano lo stesso livello sonoro. Il guadagno album può essere usato per assicurarsi che tutti gli album abbiano lo stesso livello sonoro ma che le differenze di volume tra le tracce di uno stesso album vengano mantenute. Il guadagno intelligente usa il guadagno album se i brani consecutivi appartengono allo stesso album o il guadagno traccia nel caso di una playlist mista. - NL Sommige muziekbestanden bevatten volumeaanpassing, of 'Replay Gain'-informatie, die gebruikt kan worden tijdens het afspelen om ervoor te zorgen dat nummers en albums met hetzelfde volume afgespeeld worden. Het muzieksysteem kan deze informatie gebruiken, indien aanwezig. Je kunt volumeaanpassing van tracks gebruiken om tracks met een gelijk volume af te spelen. Je kunt volumeaanpassing van albums gebruiken om albums met een gelijk volume af te spelen, maar daarbij de volumeverschillen tussen de tracks van een album bewaren. 'Slimme' aanpassingsselectie gebruikt volumeaanpassing van album als opeenvolgende nummers van hetzelfde album komen, of volumeaanpassing van tracks voor een gemengde playlist. + NL Sommige muziekbestanden bevatten volumeaanpassing, of 'Replay Gain'-informatie, die gebruikt kan worden tijdens het afspelen om ervoor te zorgen dat nummers en albums met hetzelfde volume afgespeeld worden. De muziekspeler kan deze informatie gebruiken, indien aanwezig. Je kunt volumeaanpassing van nummers gebruiken om nummers met een gelijk volume af te spelen. Je kunt volumeaanpassing van albums gebruiken om albums met een gelijk volume af te spelen, maar daarbij de volumeverschillen tussen de nummers van een album bewaren. 'Slimme' aanpassingsselectie gebruikt volumeaanpassing van album als opeenvolgende nummers van hetzelfde album komen, of volumeaanpassing van nummers voor een gemengde playlist. NO Enkelte lydspor har informasjon om volumjusteringer, eller «Replay Gain», som kan brukes under avspilling for å sikre at spor og album får samme lydnivå. Spilleren kan bruke denne informasjonen hvis den finnes. «Track gain» kan brukes til å sikre at alle sanger spilles like høyt. «Album gain» kan brukes til å sikre at album spilles med samme lydstyrke, uten at variasjoner mellom sporene blir borte. «Smart gain» bruker «Album gain» når flere sanger fra samme album spilles etter hverandre, men «Track gain» i spillelister som blander spor fra ulike album. PL Niektóre utwory audio zwierają informacje o regulacji głośności lub wzmocnieniu przy ponownym odtwarzaniu, których można użyć podczas odtwarzania w celu uzyskania jednakowej głośności odtwarzania utworów i albumów. Odtwarzacz może użyć tych informacji, jeżeli są dostępne. Opcja „Wzmocnienie utworu” umożliwia uzyskanie jednakowej głośności wszystkich utworów. Opcja „Wzmocnienie albumu” umożliwia uzyskanie jednakowej głośności albumów, ale różnice głośności między utworami w albumie zostaną zachowane. Opcja „Inteligentne” dotycząca wzmocnienia korzysta ze wzmocnienia albumu, jeżeli kolejne utwory pochodzą z tego samego albumu lub wzmocnienia utworu dla mieszanej listy odtwarzania. RU На некоторых аудиодорожках содержатся сведения выравнивания громкости (алгоритм Replay Gain), которые обеспечивают одинаковую громкость воспроизведения дорожек и альбомов. Плеер может использовать эту информацию (если она есть). При выравнивании громкости по дорожкам достигается одинаковая громкость всех дорожек. При выравнивании громкости по альбомам — одинаковая громкость для всех альбомов, при этом различия в громкости дорожек в пределах одного альбома сохраняются. При интеллектуальном выравнивании используется выравнивание по альбомам, если последующие песни входят в тот же альбом, либо выравнивание по дорожкам, если плей-лист состоит из песен разных альбомов. @@ -5473,7 +5473,7 @@ SETUP_REPLAYGAIN_REMOTE_DESC DE Internet Streaming Dienste stellen selten Werte für die Lautstärken-Normalisierung zur Verfügung. Dies kann dazu führen, dass Musikstücke von Online-Diensten erheblich lauter wiedergegeben werden, als lokale Musikstücke. Definiere einen Standard-Wert, der bei Internet-Streams verwendet werden soll. EN Remote streaming service often don't support any form of volume adjustment. If you're playing remote tracks mixed in with local tracks, this can lead to undesired volume changes. Define a default value which should be applied to any remote stream not defining a volume adjustment value itself. FR La musique en streaming sur Internet donne rarement le gain pour ajuster le volume. Cela peut conduire à subir des variations importantes de volume entre des morceaux en ligne et de la musqique en local. Définir une valeur par défaut à utiliser pour le streaming sur Internet si l'information de normalisation est absente. - NL Online streamingdiensten bieden meestal geen vorm van volumenormalisering aan. Als je deze internetstreams mixt met locale bestanden, kan dit leiden tot ongewenste veranderingen in het volume. Bepaal een standaardwaarde die bij internetstreams moet worden gebruikt. + NL Online streamingdiensten bieden meestal geen vorm van volumenormalisering aan. Internetstreams kunnen hierdoor duidelijk harder weergegeven worden dan locale bestanden. Stel een standaardwaarde in die bij internetstreams moet worden gebruikt. NO Strømmetjenester støtter ofte ikke volumjustering. Du kan få uønskede volumendringer hvis du veksler mellom spor fra strømmetjenester og lokale musikkfiler. Sett et standardvolum som skal brukes på alle internettstrømmer. PL Zdalne strumienie często nie przesyłają żadnych danych, które pozwalałyby na automatyczne dopasowanie głośności. Odtwarzanie utworów ze zdalnych strumieni wymieszanych z lokalnymi utworami może doprowadzić do niepożądanych zmian głośności. Ustal domyślną wartość, która będzie użyta dla każdego zdalnego strumienia, który nie określa własnego poziomu głośności. @@ -5487,7 +5487,7 @@ REPLAYGAIN_DISABLED FR Désactiver l'ajustement du volume HE השבתת האפשרות לכוונון עוצמת הקול IT Disattiva correzione volume - NL Volumeaanpassing uitschakelen + NL Normalisering uitschakelen NO Skru av volumkontroll PL Brak regulacji głośności RU Без выравнивания громкости @@ -5519,7 +5519,7 @@ REPLAYGAIN_TRACK_GAIN FR Utiliser le gain morceau si présent HE שימוש בהגברת רצועות (אם האפשרות קיימת) IT Guadagno brano - NL Volumeaanpassing nummers + NL Normalisering nummers NO Gain for spor PL Wzmocnienie utworu RU Выровнять по дорожкам @@ -5536,7 +5536,7 @@ REPLAYGAIN_ALBUM_GAIN FR Utiliser le gain album si présent HE שימוש בהגברת אלבום (אם האפשרות קיימת) IT Guadagno album - NL Volumeaanpassing albums + NL Normalisering albums NO Album gain PL Wzmocnienie albumu RU Выровнять по альбомам @@ -5553,7 +5553,7 @@ REPLAYGAIN_SMART_GAIN FR Gain intelligent HE בחירת הגברה חכמה IT Guadagno intelligente - NL Slimme versterking + NL Slimme normalisering NO Smart gain PL Inteligentne wzmocnienie RU Интеллектуальное усиление @@ -5570,7 +5570,7 @@ REPLAYGAIN FR Ajustement volume HE כוונון עוצמת הקול IT Correzione volume - NL Volumeaanpassing + NL Volumenormalisering NO Lydstyrkekontroll PL Regulacja głośności RU Регулировка громкости @@ -5587,7 +5587,7 @@ ALBUMREPLAYGAIN FR Ajustement volume album HE כוונון עוצמת הקול של אלבומים IT Correzione volume album - NL Volumeaanpassing voor albums + NL Volumenormalisering voor albums NO Volumjustering for album PL Regulacja głośności dla albumu RU Выравнивание громкости альбома @@ -5618,7 +5618,7 @@ SETUP_MP3STREAMINGMETHOD_DESC FI Kun toistetaan internet -virtaa, virtautus voidaan suorittaa kahdella tavalla. Jos valitaan suora virtautus, soitin muodostaa suoran yhteyden äänen virtautuspalvelimeen. Tämä vähentää Logitech Media Serverin kuormitusta lievästi. Virran toistoa voidaan myös jatkaa, jos Logitech Media Serverin yhteys katkeaa. Jos valitaan virtautus välityspalvelimen kautta, Logitech Media Server huolehtii yhteydestä ja välittää äänen soittimeen. Oletus on suora virtautus, mutta jos virtojen kanssa esiintyy ongelmia, kokeile vaihtaa välityspalvelimen kautta virtautukseen. Huomaa, että jos vähintään kaksi palvelinta on synkronoitu virtaan, käytetään aina välityspalvelimen kautta virtautusta, koska se vähentää kaistanleveyden käyttöä. FR Il existe deux manières de diffuser un flux sur Internet. La diffusion directe permet à la platine de se connecter directement au serveur de diffusion audio, réduisant ainsi la charge sur le Logitech Media Server et permettant de poursuivre la lecture du flux en cas de perte de la connexion. La diffusion par proxy permet au Logitech Media Server de gérer la connexion et de transmettre le flux audio vers la platine. La diffusion directe est l'option sélectionnée par défaut. Si vous rencontrez des problèmes lors de la lecture des flux, sélectionnez l'option de diffusion par proxy. Notez que si plusieurs platines sont synchronisées pour utiliser un flux, c'est la diffusion proxy qui est utilisée afin de réduire l'utilisation de bande passante. IT Quando si riproduce uno stream Internet si possono utilizzare due metodi. Con il metodo dello streaming diretto il riproduttore si collega direttamente al server di streaming audio. Ciò consente di ridurre leggermente il carico di Logitech Media Server e di proseguire la riproduzione dello stream anche nel caso in cui la connessione con Logitech Media Server venga persa. Con il metodo dello streaming tramite proxy la gestione della connessione viene effettuata da Logitech Media Server, il quale passa l'audio al riproduttore. Lo streaming diretto è il metodo predefinito. Se tuttavia si riscontrassero problemi con gli stream, provare a utilizzare il metodo dello streaming tramite proxy. Si noti che se due o più lettori sono sincronizzati su uno stream, viene sempre utilizzato il metodo dello streaming tramite proxy per ridurre l'utilizzo della banda. - NL Het afspelen van een internetstream kan op twee manieren. Directe streaming: het muzieksysteem maakt een directe verbinding met de server die de audio streamt. Je Logitech Media Server wordt hierdoor minder belast en de stream speelt door als de verbinding tussen het muzieksysteem en de Logitech Media Server wordt verbroken. Proxystreaming: de Logitech Media Server zorgt voor de verbinding en geeft de audiostream door aan het muzieksysteem. Directe streaming is de standaard, maar als dit problemen oplevert, kun je proxystreaming proberen. Let op: Als je twee of meer muzieksystemen synchroniseert op een stream, wordt proxystreaming automatisch ingesteld om zo de gebruikte bandbreedte te minimaliseren. + NL Het afspelen van een internetstream kan op twee manieren. Directe streaming: de muziekspeler maakt een directe verbinding met de server die de audio streamt. Je Logitech Media Server wordt hierdoor minder belast en de stream speelt door als de verbinding tussen de muziekspeler en de Logitech Media Server wordt verbroken. Proxystreaming: de Logitech Media Server zorgt voor de verbinding en geeft de audiostream door aan de muziekspeler. Directe streaming is de standaard, maar als dit problemen oplevert, kun je proxystreaming proberen. Let op: Als je twee of meer muziekspelers synchroniseert op een stream, wordt proxystreaming automatisch ingesteld om zo de gebruikte bandbreedte te minimaliseren. NO Avspilling av strømmer fra Internett kan gjøres på to måter: Hvis du bruker direktestrøm oppretter spilleren en direkte forbindelse til serveren som sender lydstrømmen. Det gir litt mindre belastning på Logitech Media Server-maskinen, og strømmen kan spilles av selv om forbindelsen til denne blir brutt. Det er også mulig å sende datastrømmen gjennom Logitech Media Server, som da opptrer som en proxy. Dette kalles proxystrømming. Direktestrømming er standard, men hvis du får problemer med strømmer, kan du prøve å endre innstillingen til proxystrømming i stedet. Merk: hvis to eller flere spillere er synkronisert med en strøm, brukes proxystrømming alltid, for å redusere båndbreddebruken. PL Podczas odtwarzania internetowego strumienia przesyłanie strumieniowe wykonywane jest na dwa sposoby. Bezpośrednie przesyłanie strumieniowe powoduje połączenie z serwerem strumieniowego przesyłania dźwięku, co nieznacznie zmniejsza obciążenie programu Logitech Media Server i umożliwia kontynuację odtwarzania strumienia, jeżeli połączenie z programem Logitech Media Server zostanie utracone. Przesyłanie strumieniowe z wykorzystaniem serwera proxy powoduje, że program Logitech Media Server obsługuje połączenie, przekazując dźwięk do odtwarzacza. Bezpośrednie przesyłanie strumieniowe to metoda domyślna, ale w przypadku problemów ze strumieniami należy zmienić ustawienie na Przesyłanie strumieniowe przez serwer proxy. Należy pamiętać, że jeżeli ze strumieniem zsynchronizowane są co najmniej dwa odtwarzacze, zawsze używane jest przesyłanie strumieniowe przez serwer proxy w celu ograniczenia użycia przepustowości. RU Поддерживается два способа потоковой передачи из Интернета. При прямой передаче плеер напрямую подключается к серверу потокового аудио, что несколько уменьшает нагрузку на Logitech Media Server и позволяет продолжать воспроизведение потока в случае разрыва подключения к Logitech Media Server. При передаче через прокси Logitech Media Server обслуживает подключение, передавая аудиоданные плееру. По умолчанию задана прямая потоковая передача, но если возникают проблемы с потоком, можно попробовать переключиться на опосредованную передачу. Помните: если два и более плееров настраиваются на один поток, всегда применяется опосредованная передача для сужения полосы пропускания. @@ -5680,7 +5680,7 @@ SETUP_OUTPUT_CHANNELS_DESC FI Voit määrittää soittimen toistamaan vain vasemmalla tai oikealla kanavalla. Näin voidaan luoda stereovaikutelma esimerkiksi kahdella synkronoidulla Squeezebox-radiolla. FR Vous pouvez configurer cette platine pour lire uniquement le canal droit ou gauche. Cette configuration vous permet par exemple de créer une image stéréo à partir de deux radios Squeezebox synchronisées. IT È possibile configurare il lettore affinché riproduca solo il canale destro o sinistro. Ciò risulta utile ad esempio per creare un'immagine stereo da due Squeezebox Radio sincronizzate. - NL Je kunt dit muzieksysteem configureren zodat alleen via het linker- of rechterkanaal wordt afgespeeld. Zo kun je bijvoorbeeld 2 gesynchroniseerde Squeezebox Radio's in stereo-opstelling plaatsen. + NL Je kunt deze muziekspeler configureren zodat alleen via het linker- of rechterkanaal wordt afgespeeld. Zo kun je bijvoorbeeld 2 gesynchroniseerde Squeezebox Radio's in stereo-opstelling plaatsen. NO Du kan konfigurere spilleren slik at den kun spiller fra venstre eller høyre kanal. Slik kan du blant annet skape et stereobilde fra to synkroniserte Squeezebox Radio-spillere. PL Ten odtwarzacz można skonfigurować tak, aby odtwarzał tylko kanał prawy lub lewy. W ten sposób można na przykład utworzyć obraz stereo z 2 zsynchronizowanych odtwarzaczy Squeezebox Radio. RU Можно настроить этот плеер для воспроизведения только левого или правого канала. Например, с помощью этой функции можно создать стереоэффект, используя два синхронизированных радио Squeezebox. @@ -5758,7 +5758,7 @@ SETUP_TRANSITIONTYPE_DESC FR La Squeezebox peut joindre deux morceaux par un effet de transition (ou fondu): un fondu enchaîné ou une combinaison de fondus descendant et ascendant. Sélectionnez le type de transition souhaité. HE הנגן יכול לעבור באופן הדרגתי בין שירים, בין אם באמצעות מעבר הדרגתי או הגברה או החלשה של שירים. בחר את סוג המעבר ההדרגתי הרצוי. IT Il lettore può passare gradualmente tra i brani, sia sfumando tra il precedente ed il successivo, sia dissolvendo in entrata o in uscita. Scegliere il tipo di transizione desiderato. - NL Het muzieksysteem kan naadloze overgangen tussen nummers maken door crossfading tussen de nummers, of door de nummers in en uit te faden. Kies het gewenste type crossfade. + NL De muziekspeler kan naadloze overgangen tussen nummers maken door crossfading tussen de nummers, of door de nummers in en uit te faden. Kies het gewenste type crossfade. NO Spilleren kan lage overganger mellom sanger, enten med krysstoning eller ved å tone sanger ut og så inn. Velg hva slags overgang du vil ha. PL Odtwarzacz może stopniowo przechodzić między utworami za pomocą przenikania lub stopniowego zgłaśniania i wyciszania. Wybierz odpowiedni typ przenikania. RU Плеер может плавно переходить между песнями: либо с помощью перекрестного затухания, либо с помощью усиления или затухания песен. Выберите необходимый тип перехода. @@ -5798,6 +5798,18 @@ SETUP_TRANSITIONDURATION_DESC RU Введите временной интервал для перекрестного затухания между песнями (в секундах). Максимум — 10 с. SV Ange varaktighet för korstoningen i sekunder. Maximalt värde är 10 sekunder. ZH_CN 请以秒为单位输入在歌曲之间音频同时淡出淡入的持久时间。最大值为10秒。 + +SETUP_FADEINDURATION + DE Dauer der Einblendung + EN Play or Resume fade-in duration + FR Durée de montée en volume + NL Duur infaden + +SETUP_FADEINDURATION_DESC + DE Die Lautstärke kann eingeblendet werden wenn die Titel-Wiedergabe gestartet wird. Im Gegensatz zur Überblendung gilt dies nur für den Start der Wiedergabe. + EN When a track is played or resumed, volume can be progressively increased. This is not like crossfade which only happen between tracks. + FR Lors du démarrage or de la reprise de la lecture, le volume peut être augmenté progressivement. Ce n'est pas la transition entre pistes. + NL Het volume van een gestart nummer kan toenemend worden verhoogd (infaden). Dit in tegestelling tot crossfade, waar dit tussen de nummers gebeurt. TRANSITION_NONE CS Žádný @@ -6100,7 +6112,7 @@ SETUP_SHOWYEAR_DESC HE באפשרותך לבחור להציג את שנת הפרסום של האלבום במהלך עיון באלבומים, או בעטיפות אלבומים. כאשר אפשרות זו מופעלת, השנה של פרסום האלבום תוצג ליד שם האלבום. IT È possibile scegliere di visualizzare l'anno quando si sfoglia per album o per copertina. Se abilitato, l'anno dell'album verrà mostrato di fianco al titolo dell'album. JA アルバムまたはアートワークで検索する時に、アルバム年を表示することができます。アルバムタイトルの横に表示されます。 - NL Je kunt het albumjaar laten weergeven wanneer je albums of hoesafbeeldingen bekijkt. Is deze optie ingeschakeld, dan wordt het albumjaar naast de albumtitel weergegeven. + NL Je kunt het albumjaar laten weergeven wanneer je albums of albumcovers bekijkt. Wanneer ingeschakeld, wordt het albumjaar naast de albumtitel weergegeven. NO Du kan velge å vise albumets utgivelsesår når du blar etter album eller albumomslag. Når dette er aktivert, vises utgivelsesåret ved siden av tittelen på albumet. PL Możliwe jest włączenie wyświetlania roku albumu podczas przeglądania według albumów lub okładek. Po włączeniu rok albumu zostanie wyświetlony obok tytułu albumu. RU При просмотре альбомов или обложек можно выводить и год выпуска альбома. Когда этот параметр включен, год выпуска альбома будет отображаться рядом с его названием. @@ -6239,7 +6251,7 @@ SETUP_NO_PREFS FI Soittimelle ei ole asetuksia tällä sivulla FR Aucun réglage n'est disponible pour cette platine sur cette page. IT Nessuna impostazione disponibile per questo lettore in questa pagina - NL Er zijn geen instellingen voor dit muzieksysteem op deze pagina + NL Geen instellingen voor deze muziekspeler op deze pagina NO Det er ingen innstillinger for denne spilleren på denne siden PL Na tej stronie nie ma ustawień dla tego odtwarzacza RU На странице нет настроек для этого плеера @@ -6407,7 +6419,7 @@ SETUP_ARTFOLDER HE תיקיית עטיפות IT Cartella copertine JA アートワークのフォルダー - NL Map met hoesafbeeldingen + NL Map met albumcovers NO Mappe for albumomslag PL Folder okładek RU Папка "Обложки" @@ -6425,7 +6437,7 @@ SETUP_ARTFOLDER_DESC HE באפשרותך לבחור לאחסן את כל העטיפות בתיקייה יחידה, באמצעות האפשרות לשינוי שמות קבצים שלעיל. הזן כאן את המיקום של קובצי העטיפות. ללא תלות במיקום זה, השרת עדיין יחפש עטיפות בנתיב של כל קובץ שמע אם לא נמצאו עטיפות מתאימות בתיקיית העטיפות. IT Si può scegliere di archiviare tutte le copertine in una sola cartella, usando l'opzione per i nomi di file variabili indicata sopra. Inserire qui la posizione dei file delle copertine. Se nella cartella delle copertine non viene trovata alcuna copertina corrispondente, ne viene eseguita una ricerca nello stesso percorso dei file audio . JA アートワークを1つのフォルダーにまとめて保存しても構いません。以下に入力してください。フォルダー名を指定しても、もし見つからなければ、音楽ファイルがある場所を探します。 - NL Je kunt alle hoesafbeeldingen in één map opslaan via de optie voor variabele bestandsnamen hierboven. Typ hier de locatie van de bestanden met hoesafbeeldingen. Ongeacht wat je hier invult, zal Logitech Media Server ook nog in de map met de audiobestanden kijken, als in de map met hoesafbeeldingen geen hoesje is gevonden. + NL Je kunt alle albumcovers in één map opslaan via de optie voor variabele bestandsnamen hierboven. Typ hier de locatie van de bestanden met albumcovers. Ongeacht wat je hier invult, zal Logitech Media Server ook nog in de map met de audiobestanden kijken, als die in de map met albumcovers niet is gevonden. NO Med alternativet for variable filnavn ovenfor, kan du velge å lagre alle albumomslag i samme mappe. Angi hvor du vil lagre albumomslagsfilene her. Uansett hvor du plasserer mappen, kommer Logitech Media Server først til å se etter albumomslag i samme bane som der hver lydfil ligger, hvis den ikke finner riktig omslag i albumomslagsmappen. PL Wszystkie okładki można zapisać w jednym folderze, używając powyższej opcji zmiennych nazw plików. Wprowadź lokalizację plików okładek tutaj. Bez względu na tę lokalizację, jeżeli w folderze okładek nie zostanie znaleziona odpowiednia okładka, program Logitech Media Server będzie szukał okładki, używając ścieżki do pliku audio. RU Можно хранить все обложки в единой папке, пользуясь вышеописанной функцией изменяемых имен файлов. Введите в это поле расположение файлов обложек. Независимо от данного расположения Logitech Media Server будет по-прежнему выполнять поиск обложек в папке каждого аудиофайла, если в папке обложек не обнаружится необходимых обложек. @@ -6480,7 +6492,7 @@ SETUP_PLAYLISTDIR_DESC HE באפשרותך להזין את הנתיב לספרייה שבה מאוחסנים הקבצים של רשימות ההשמעה השמורות בדיסק הקשיח. (אם אינך מעוניין לשמור רשימות השמעה, באפשרותך להשאיר שדה זה ריק.) IT È possibile specificare il percorso sul disco rigido in cui salvare i file delle playlist. Se non si desidera salvare le playlist, lasciare vuoto questo campo. JA プレイリストが保存されているフォルダーへのパスを入力してください(プレイリストを保存しないのであればブランクで構いません)。 - NL Je kunt het pad invoeren naar een map waarin je playlistbestanden op je vaste schijf moeten worden opgeslagen. (Je kunt dit leeg laten als je geen playlists wilt opslaan.) + NL Je kunt het pad invoeren naar een map waarin je playlists op je vaste schijf moeten worden opgeslagen. (Je kunt dit leeg laten als je geen playlists wilt opslaan.) NO Her kan du angi banen til katalogen der du har lagret spillelistefilene dine på harddisken. (Du kan la dette feltet stå tomt hvis du ikke vil lagre spillelister.) PL Możliwe jest wprowadzenie ścieżki do katalogu przechowywania zapisanych list odtwarzania na dysku twardym. (Pole można zostawić puste, jeżeli listy odtwarzania nie mają być zapisywane.) PT A directoria onde serão guardadas as playlists. (Pode deixar vazio se não quiser guardar playlists) @@ -6829,7 +6841,7 @@ SETUP_REFRESHRATE HE תזמון לרענון הדפדפן IT Intervallo aggiornamento browser JA ブラウザーを更新するタイミング - NL Vernieuwingstiming voor browser + NL Refresh-interval browser NO Oppdateringsintervall for leser PL Czas odświeżania przeglądarki RU Интервал обновления браузера @@ -6865,7 +6877,7 @@ SETUP_COVERART HE עטיפה IT Copertine JA アートワーク - NL Hoesafbeeldingen + NL Albumcovers NO Albumomslag PL Okładka PT Capa Álbum @@ -6884,7 +6896,7 @@ SETUP_COVERART_DESC HE תמונות של עטיפות אלבומים, כאשר זמינות, מופיעות בעת הצגה של פרטי אלבום או שיר. תמונות של עטיפות נמצאות בתגים מסוג ID3 עבור שירים נפרדים, או מאוחסנות באותה תיקייה עם קובצי השירים. כברירת מחדל, נעשה שימוש בקבצים בשם "cover.jpg"‏, "albumartsmall.jpg"‏, "folder.jpg"‏, "album.jpg" או "thumb.jpg". באפשרותך לציין שם קובץ נוסף שישמש עבור תמונות של עטיפות אלבומים. באפשרותך לציין שמות קובץ שונים עבור תמונות ממוזערות או תמונות בגודל מלא. הוסף את הקידומת % לאפשרות וציין לאחר מכן כל מחרוזת המורכבת מאותם רכיבים הזמינים עבור תבניות שם (לדוגמה, ‎%ARTIST - ALBUM יחפש את Artist - Album.jpg בהתאם למידע המבצע והאלבום שנמצא.) IT Le immagini delle copertine, se disponibili, sono visualizzate insieme alle informazioni su un album o un brano. Le copertine possono essere inserite nei tag ID3 di ogni singolo brano o risiedere nella stessa cartella dei file dei brani. Per impostazione predefinita vengono utilizzate immagini denominate cover.jpg, folder.jpg, album.jpg o thumb.jpg. È possibile specificare un nome di file aggiuntivo da utilizzare per le copertine e nomi diversi per le miniature e le immagini intere. Anteporre all'opzione il carattere %, a cui può far seguito qualsiasi stringa composta dagli stessi elementi disponibili per il formato titolo. Ad esempio, con %ARTIST - ALBUM viene cercato il file Artista - Album.jpg corrispondente alle informazioni sull'artista e sull'album trovati. JA アルバムのアートワークがある場合には、アルバム/曲情報を見るときに表示されます。アートワーク イメージはそれぞれの曲のID3タグか、曲のファイルと同じフォルダーに入っています。工場出荷設定では"cover.jpg", "folder.jpg", "album.jpg", "thumb.jpg" が使われています。これらアルバム アートワークのファイル名は変更することができます。例えば%ARTIST - ALBUM, は Artist - Album.jpg を探します。 - NL Als er een hoesafbeelding beschikbaar is, wordt deze getoond wanneer je informatie over een album of nummer bekijkt. Deze afbeeldingen staan in de ID3-tags voor afzonderlijke nummers of in dezelfde map als muziekbestanden. Standaard worden afbeeldingen met de volgende namen gebruikt: folder.jpg, album.jpg of thumb.jpg. Je kunt een aanvullende bestandsnaam opgeven voor hoesafbeeldingen. Je kunt andere bestandsnamen opgeven voor miniatuurafbeeldingen of afbeeldingen op volledig formaat. Wanneer je het voorvoegsel % voor de optie zet, kun je er een willekeurige string op laten volgen die uit dezelfde elementen als titelformaten bestaat (bijv. %ARTIST - ALBUM zoekt naar 'Artist - Album.jpg' om artiest- en albuminformatie te vinden). + NL Als er een albumcover beschikbaar is, wordt deze getoond wanneer je informatie over een album of nummer bekijkt. Deze afbeeldingen staan in de ID3-tags voor afzonderlijke nummers of in dezelfde map als muziekbestanden. Standaard worden afbeeldingen met de volgende namen gebruikt: folder.jpg, album.jpg of thumb.jpg. Je kunt een aanvullende bestandsnaam opgeven voor albumcovers. Je kunt andere bestandsnamen opgeven voor miniatuurafbeeldingen of afbeeldingen op volledig formaat. Wanneer je het voorvoegsel % voor de optie zet, kun je er een willekeurige string op laten volgen die uit dezelfde elementen als titelformaten bestaat (bijv. %ARTIST - ALBUM zoekt naar 'Artist - Album.jpg' om artiest- en albuminformatie te vinden). NO Hvis albumomslag er tilgjengelig, kan dette vises når du ser informasjon om et album eller en sang. Bildefiler med albumomslag finnes i ID3-etikettene for enkeltsanger, eller de kan ligge i samme mappe som sangfilene. Bildefiler med navn som «cover.jpg», «folder.jpg», «album.jpg» eller «thumb.jpg» brukes som standard. Du kan angi et annet mulig navn på bildefilene for albumomslag, og også angi egne navn på filer med miniatyrversjoner av bildene. Hvis du skriver % før alternativet, kan du legge inn enhver streng med de samme elementene som er tilgjengelige for tittelformater etter det. (Eksempel: %ARTIST - ALBUM gjør at det søkes etter en fil som heter Artist – Album.jpg ut fra informasjon om artist og album som er å finne. PL Obrazy okładki albumu (jeżeli są dostępne) pojawiają się po wyświetleniu informacji o albumie lub utworze. Obrazy okładek znajdują się w znacznikach ID3 poszczególnych utworów lub w tym samym folderze co pliki utworów. Domyślnie używane są obrazy o nazwie „cover.jpg”, „folder.jpg”, „album.jpg” lub „thumb.jpg”. Możliwe jest określenie dodatkowej nazwy pliku używanej dla okładek albumów. Można także określić inne nazwy plików dla miniatur i obrazów pełnowymiarowych. Poprzedzenie opcji znakiem % umożliwia dodanie dowolnego ciągu złożonego z tych samych elementów, jakie są używane w formatach tytułów (np. wpisanie tekstu %ARTIST - ALBUM spowoduje wyszukiwanie pliku Artist - Album.jpg w celu dopasowania do znalezionych informacji o wykonawcy i albumie. PT As imagens da capa do álbum, quando disponíveis, são mostradas na visualização da informação do álbum ou da música. As capas dos álbuns poderão estar integradas nas "tags ID3" do próprio ficheiro, ou em ficheiros separados na pasta dos ficheiros de música. Por omissão, serão usados os nomes de ficheiros "cover.jpg", "folder.jpg", "album.jpg" ou "thumb.jpg". Pode especificar nomes de ficheiros adicionais para a imagem da capa do álbum. Pode especificar nomes diferentes para a miniatura ou para a imagem completa. @@ -6901,7 +6913,7 @@ SETUP_DOWNLOAD_ARTWORK FI Viimeistele kansitaide automaattisesti FR Rechercher automatiquement la pochette IT Completamento automatico copertina - NL Hoesafbeeldingen automatisch invullen + NL Albumcovers automatisch aanvullen NO Finn fram omslag automatisk PL Autouzupełnianie okładek RU Автоматически заполнить обложку @@ -6916,7 +6928,7 @@ SETUP_DOWNLOAD_ARTWORK_DESC FI Logitech Media Server voi yrittää viimeistellä musiikkitiedostojen puuttuvan kansitaiteen, kun kirjastoa tarkistetaan. Esittäjän ja albumin tiedot lähetetään osoitteeseen mysqueezebox.com, josta täsmäävää kansitaidetta etsitään. Ota huomioon, että tämä saattaa hidastaa tarkistusta huomattavasti.

Kansitaiteen tarjoaa Last.fm. FR Le Logitech Media Server peut essayer de rechercher les pochettes manquantes de vos fichiers musicaux pendant qu'il analyse votre bibliothèque. Pour ce faire, il envoie les informations des artistes et des albums à mysqueezebox.com pour trouver les pochettes correspondantes. Veuillez noter que ce processus peut considérablement ralentir l'analyse.

Les pochettes sont fournies par Last.fm. IT È possibile tentare di inserire le copertine mancanti per i file musicali mediante Logitech Media Server durante l'analisi della libreria. Tale processo prevede l'invio di informazioni su album e artisti a mysqueezebox.com per individuare le copertine corrispondenti. Il processo può rallentare notevolmente l'analisi.

Le copertine vengono fornite da Last.fm. - NL Logitech Media Server kan ontbrekende hoesafbeeldingen voor je opzoeken tijdens het scannen van je collectie. Hierbij worden gegevens van de artiest en het album verzonden naar mysqueezebox.com om te zien of er hoesafbeeldingen beschikbaar zijn. Houd er wel rekening mee dat dit het scanproces aanzienlijk kan vertragen.

Hoesafbeeldingen worden aangeboden door Last.fm. + NL Logitech Media Server kan ontbrekende albumcovers voor je opzoeken tijdens het scannen van je collectie. Hierbij worden gegevens van de artiest en het album verzonden naar mysqueezebox.com om te zien of er albumcovers beschikbaar zijn. Houd er wel rekening mee dat dit het scanproces aanzienlijk kan vertragen.

Albumcovers worden aangeboden door Last.fm. NO Logitech Media Server kan forsøke å sette inn manglende omslag for musikkfilene dine under søk i biblioteket. Denne prosessen sender artist- og albuminformasjon til mysqueezebox.com for å finne fram til riktig albumomslag. Prosessen kan føre til at søket tar betraktelig lengre tid.

Det er Last.fm som leverer albumomslag. PL Program Logitech Media Server może spróbować uzupełnić brakujące okładki do plików muzycznych podczas przeszukiwania biblioteki. W ramach tego procesu informacje o wykonawcy i albumie zostaną przesłane do witryny mysqueezebox.com w celu wykrycia pasującej okładki. Uwaga: proces ten może znacząco spowolnić przeszukiwanie biblioteki.

Okładki udostępnia serwis Last.fm. RU Logitech Media Server может попробовать найти недостающие обложки для музыкальных файлов в процессе сканирования медиатеки. При этом сведения об исполнителе и альбоме будут отправляться на mysqueezebox.com, чтобы найти соответствующую обложку. Нужно учитывать, что этот процесс может значительно замедлить сканирование.

Обложки предоставляются сайтом Last.fm. @@ -6931,7 +6943,7 @@ SETUP_DOWNLOAD_ARTWORK_ON FI Lataa albumin kansitaide (kokeellinen) FR Télécharger la pochette de l'album (expérimental) IT Scarica copertine album (sperimentale) - NL Hoesafbeeldingen van albums downloaden (experiment) + NL Albumcovers downloaden (experiment) NO Last ned albumomslag (eksperimentell) PL Pobierz okładki albumów (funkcja eksperymentalna) RU Загрузить обложку альбома (экспериментально) @@ -6946,7 +6958,7 @@ SETUP_DOWNLOAD_ARTWORK_OFF FI Älä lataa albumin kansitaidetta FR Ne pas télécharger la pochette de l'album IT Non scaricare le copertine degli album - NL Hoesafbeeldingen van albums niet downloaden + NL Albumcovers niet downloaden NO Ikke last ned albumomslag PL Nie pobieraj okładek albumów RU Не загружать обложку альбома @@ -6981,7 +6993,7 @@ SETUP_THUMBSIZE_DESC HE בחר את הגודל בפיקסלים (25-250) עבור התמונה הממוזערת המוגדרת כברירת מחדל בעת עיון לפי עטיפה. ברירת המחדל היא 100. IT Scegliere la dimensione predefinita in pixel (25-250) delle miniature quando si sfogliano le copertine. Il valore predefinito è 100. JA アートワークでブラウズする場合の、サムネイルの大きさをピクセル数(25-250)で指定してください。工場出荷時は100です。 - NL Kies de grootte in pixels (25-250) voor de standaard miniatuurweergave wanneer je hoesafbeeldingen bekijkt. De standaardwaarde is 100. + NL Kies de grootte in pixels (25-250) voor de standaard miniatuurweergave wanneer je albumcovers bekijkt. De standaardwaarde is 100. NO Velg størrelse i piksler (25–250) som miniatyrer av albumomslag skal vises i som standard. Standardverdien er 100 piksler. PL Wybierz rozmiar w pikselach (25-250) domyślnego obrazu miniatury podczas przeglądania okładek. Wartość domyślna to 100. RU Выберите размер в пикселях (25–250) для изображения эскиза, используемый по умолчанию при просмотре обложек. Значение по умолчанию — 100. @@ -7018,7 +7030,7 @@ SETUP_HTTPPORT_DESC HE באפשרותך לשנות את מספר היציאה המשמש לגישה לשרת מדפדפן אינטרנט. (ברירת המחדל היא 9000.) IT Per l'accesso al server tramite un browser Web è possibile impostare un numero di porta diverso da quello utilizzato. Il numero di porta predefinito è 9000. JA ウェブ ブラウザーからサーバーにアクセスする為のポート ナンバーを変更することができます (標準は9000)。 - NL Je kunt het poortnummer veranderen dat gebruikt wordt om de server via een webbrowser op te roepen. (De standaardpoort is 9000.) + NL Je kunt het poortnummer wijzigen dat gebruikt wordt om de server via een webbrowser op te roepen. (Standaardpoort is 9000.) NO Du kan endre portnummeret som brukes for å nå serveren fra en nettleser. (Standardverdi er 9000.) PL Numer portu używany w celu uzyskania dostępu do serwera z przeglądarki internetowej można zmienić (domyślny to 9000). PT Pode mudar a porta usada para aceder ao servidor usando um navegador web. (Por omissão é 9000) @@ -7280,7 +7292,7 @@ SETUP_AUTO_DOWNLOAD_DESC FI Lataa Logitech Media Serverin päivitykset automaattisesti, kun ne ovat saatavilla. Ohjelma kehottaa asentamaan päivityksen heti, kun se on ladattu. FR Télécharger automatiquement les mises à jour du Logitech Media Server disponibles. Vous serez ensuite invité à installer la mises à jour à la fin du téléchargement. IT Scarica automaticamente gli aggiornamenti a Logitech Media Server quando sono disponibili. Al termine del download verrà chiesto di installare l'aggiornamento. - NL Updates voor Logitech Media Server automatisch downloaden wanneer deze beschikbaar zijn. Je wordt dan gevraagd de updates te installeren zodra het downloaden voltooid is. + NL Beschikbare updates voor Logitech Media Server automatisch downloaden. Je wordt dan gevraagd de updates te installeren zodra het downloaden is voltooid. NO Last ned Logitech Media Server-oppdateringer automatisk når de blir tilgjengelige. Du blir bedt om å installere oppdateringen når nedlastingen er fullført. PL Automatyczne pobieraj aktualizacje programu Logitech Media Server, gdy będą dostępne. Po zakończeniu pobierania zostanie wyświetlony monit o zainstalowanie aktualizacji. RU Автоматически загружать доступные обновления Logitech Media Server. После окончания загрузки обновления появится предложение установить его. @@ -7310,7 +7322,7 @@ SETUP_AUTO_DOWNLOAD_1 FI Lataa päivitykset automaattisesti, kun ne ovat saatavilla. FR Télécharger automatiquement les mises à jour disponibles IT Scarica automaticamente gli aggiornamenti quando sono disponibili. - NL Updates automatisch downloaden wanneer deze beschikbaar zijn. + NL Beschikbare updates automatisch downloaden. NO Last ned oppdateringer automatisk når de blir tilgjengelige. PL Automatycznie pobieraj aktualizacje, gdy są dostępne. RU Автоматически загружать доступные обновления. @@ -7427,7 +7439,7 @@ SETUP_ALARMSAVER FI Näytönsäästäjä hälytyksen aikana FR Ecran de veille (alarme) IT Screen saver quando suona la sveglia - NL Schermbeveiliger wanneer wekker afgaat + NL Schermbeveiliger tijdens wekker NO Skjermsparer når alarmen ringer PL Wygaszacz ekranu podczas odtwarzania alarmu RU Экранная заставка при звучании будильника @@ -7442,7 +7454,7 @@ SETUP_ALARMSAVER_ABBR FI Hälytyksen aikana FR Lors d'une alarme IT Durante la sveglia - NL Wanneer wekker afgaat + NL Tijdens wekker NO Ved alarm PL Podczas alarmu RU Во время работы будильника @@ -7458,7 +7470,7 @@ SETUP_SCREENSAVER_DESC FR Vous pouvez sélectionner un écran de veille à afficher après un certain temps d'inactivité de la télécommande. Vous pouvez choisir la durée d'inactivité ainsi que le type d'écran de veille à afficher selon que la platine soit en lecture, arrêtée ou éteinte. HE באפשרותך לבחור שומרי מסך להצגת מידע לאחר פרק זמן מסוים של חוסר פעילות מהשלט-רחוק. באפשרותך לבחור את פרק הזמן להמתנה לפני הפעלת שומר המסך. באפשרותך גם לבחור שומרי מסך שונים בהתאם למצב הנגן - שמע מופעל, שמע מופסק או כבוי. IT È possibile scegliere uno screen saver che visualizza informazioni dopo un certo periodo di tempo di inattività del telecomando. È possibile scegliere l'attesa prima di attivare lo screen saver e scegliere screen saver differenti a seconda che il lettore stia riproducendo musica, sia stato interrotto oppure sia spento. - NL Je kunt schermbeveiligers gebruiken om informatie weer te geven nadat de afstandsbediening enige tijd niet is gebruikt. Je kunt bepalen hoe lang er gewacht moet worden voordat de schermbeveiliger ingeschakeld wordt. Je kunt ook verschillende schermbeveiligers kiezen, afhankelijk van de status van het muzieksysteem: actief, gestopt of uit. + NL Je kunt schermbeveiligers gebruiken om informatie weer te geven nadat de afstandsbediening enige tijd niet is gebruikt. Je kunt bepalen hoe lang er gewacht moet worden voordat de schermbeveiliger ingeschakeld wordt. Je kunt ook verschillende schermbeveiligers kiezen, afhankelijk van de status van de muziekspeler: actief, gestopt of uit. NO Når fjernkontrollen ikke er blitt brukt på en stund, kan spilleren vise ulike typer informasjon som «skjermsparer». Du kan velge ulike skjermsparere for ulike typer spillerstatus, dvs. om spilleren spiller eller ikke, eller om den er avskrudd. Du kan også velge hvor lenge fjernkontrollen skal være inaktiv før skjermsparerne aktiveres. PL Po okresie nieaktywności pilota zdalnego sterowania odtwarzacz może wyświetlić różne informacje takie jak „wygaszacz ekranu”. Możliwe jest wyświetlenie różnych wygaszaczy, w zależności od tego czy odtwarzacz odtwarza muzykę lub jest wyłączony. Możliwe jest także ustawienie czasu przed włączeniem wygaszacza. RU Если некоторое время с пульта ДУ не поступает никаких команд, плеер может проигрывать разную информацию в качестве экранных заставок. Вы можете выбрать экранные заставки, которые будут отображаться в зависимости от того, играет плеер или нет, и от того, выключен ли он. Также можно задать интервал времени перед появлением экранной заставки. @@ -7565,7 +7577,7 @@ SETUP_DISPLAYTEXTTIMEOUT_DESC HE כאשר אתה מזין טקסט לחיפוש, הנגן עובר אל האות הבאה לאחר פרק זמן מוגדר. באפשרותך לשנות את משך ההמתנה שמצוין להלן. IT Quando si inserisce il testo per una ricerca, il lettore si sposta sulla lettera successiva dopo un determinato periodo di tempo. È possibile modificare il tempo di attesa di seguito. JA テキストを入力して曲をサーチする場合、プレーヤーは一定の時間後、次の文字に移ります。以下で、そのタイミングを設定してください。 - NL Wanneer je tekst voor een zoekopdracht invoert, springt het muzieksysteem na een aantal seconden naar de volgende letter. Je kunt deze wachttijd hieronder wijzigen. + NL Wanneer je tekst voor een zoekopdracht invoert, springt de muziekspeler na een aantal seconden naar de volgende letter. Je kunt deze wachttijd hieronder wijzigen. NO Når du skriver inn søketekst, hopper spilleren automatisk til neste bokstavfelt etter en liten pause. Du kan angi hvor lang denne pausen skal være i sekunder. PL Podczas wprowadzania tekstu do wyszukania odtwarzacz po pewnym czasie przechodzi do następnej litery. Możliwa jest zmiana czasu oczekiwania (w sekundach). PT Ao introduzir texto para uma procura, ao fim de algum tempo será seleccionado o caracter seguinte. Pode alterar a duração desta espera abaixo. @@ -7681,7 +7693,7 @@ SETUP_NOGENREFILTER FR Portée du filtre sur les genres HE סנן סגנונות ברמת האלבום והרצועה. IT Filtra i generi musicali a livello di album e brani. - NL Genres op album- en trackniveau filteren. + NL Genres op album- en nummerniveau filteren. NO Filtrer sjangere på album- og spornivå. PL Filtruj gatunki na poziomie albumu i utworu. RU Фильтровать жанры по альбому и дорожкам. @@ -7698,7 +7710,7 @@ SETUP_NOGENREFILTER_1 FR Afficher tous les albums et morceaux de l'artiste HE הצגת כל הרצועות והאלבומים של מבצע IT Mostra qualsiasi brano e album di un artista - NL Alle tracks en albums voor een artiest weergeven + NL Alle nummers en albums voor een artiest weergeven NO Vis alle spor og album for en artist PL Pokaż dowolne utwory i albumy danego wykonawcy RU Показать дорожки и альбомы исполнителя @@ -7715,7 +7727,7 @@ SETUP_NOGENREFILTER_0 FR Afficher uniquement les albums et morceaux du genre sélectionné HE הצגה של רצועות או אלבומים התואמים לסגנון הנבחר של מבצע בלבד IT Mostra solo brani o album corrispondenti al genere selezionato per un artista - NL Alleen tracks of albums weergeven die aan het geselecteerde genre voor een artiest voldoen + NL Alleen nummers of albums weergeven die aan het geselecteerde genre voor een artiest voldoen NO Kun vis spor eller album som passer den angitte sjanger for en artist PL Pokazuj tylko utwory lub albumy zgodne z wybranym gatunkiem lub wykonawcą RU Только дорожки или альбомы, совпадающие с выбранным жанром исполнителя @@ -7732,7 +7744,7 @@ SETUP_NOGENREFILTER_DESC FR Le contenu de Parcourir les genres peut être filtré afin de n'afficher que les albums et les morceaux correspondant au genre sélectionné. HE ניתן לבצע סינון בעת עיון בסגנונות להצגת האלבומים והרצועות התואמים לסגנון הנבחר בלבד. IT È possibile applicare filtri quando si sfoglia per genere musicale in modo da mostrare solo gli album e i brani che corrispondono al genere selezionato. - NL Je kunt filtering toepassen bij 'Genres bekijken', zodat alleen de albums en tracks weergegeven worden die aan het geselecteerde genre voldoen. + NL Je kunt filtering toepassen bij 'Genres bekijken', zodat alleen de albums en nummers weergegeven worden die aan het geselecteerde genre voldoen. NO Du kan bruke filtrering når du søker i sjangere for å kun vise album og spor som passer den angitte sjangeren. PL Filtrowania można użyć podczas przeglądania gatunków tak, aby wyświetlane były tylko albumy i utwory zgodne z wybranym gatunkiem. RU При просмотре жанров можно применить фильтр, чтобы отображались только альбомы и дорожки определенного жанра. @@ -7820,7 +7832,7 @@ SETUP_PERSISTPLAYLISTS HE שמירת רשימות השמעה של מחשב הלקוח IT Mantieni playlist JA プレーヤーのプレイリストを維持する - NL Client-playlists bijhouden + NL Playlist muziekspeler bijhouden NO Husk klientspillelister PL Zachowaj listy odtwarzania klienta PT Manter as Playlists dos Clientes @@ -7896,7 +7908,7 @@ SETUP_RESHUFFLEONREPEAT HE סדר אקראי חדש בהשמעה חוזרת IT Ripetizione con ordine casuale JA リピートする時には、再度ランダムをかける - NL Volgorde veranderen bij herhaling + NL Volgorde wijzigen bij herhaling NO Omrokker på nytt ved repetisjon PL Zmień kolejność losowo przy powtarzaniu PT Misturar Lista ao Repetir @@ -7915,7 +7927,7 @@ SETUP_RESHUFFLEONREPEAT_DESC HE השרת יכול לסדר מחדש בסדר אקראי רשימות השמעה שכבר מסודרות בסדר אקראי לאחר כל מעבר מלא על הרשימה, אם אתה מעוניין בכך. IT Se lo si desidera, Logitech Media Server può ripetere la riproduzione casuale delle playlist dopo la riproduzione di ciascuna di esse. JA サーバーソフトウェアは、リピート時に、再度ランダムをかけるよう選択することができます。 - NL Logitech Media Server kan de volgorde van nummers in een geshuffelde playlist telkens nadat de lijst afgespeeld is, opnieuw veranderen. + NL Logitech Media Server kan de volgorde van nummers in een geshuffelde playlist telkens nadat de lijst afgespeeld is, opnieuw wijzigen. NO Logitech Media Server kan omrokkere spillelister på nytt etter hver gjennomspilling av en liste. PL Program Logitech Media Server może, w razie potrzeby, zmienić kolejność utworów w liście odtwarzanej losowo, każdorazowo po odtworzeniu wszystkich utworów z listy. PT O servidor pode re-misturar a lista de músicas no final dela, se necessário. @@ -7953,7 +7965,7 @@ SETUP_RESHUFFLEONREPEAT_1 HE סדר אקראי חדש בכל פעם IT Riproduzione casuale ogni volta JA 毎回、再度ランダムをかける - NL Volgorde telkens veranderen + NL Volgorde telkens wijzigen NO Omrokker listen hver gang PL Losowo za każdym razem PT Misturar Lista Sempre @@ -8048,7 +8060,7 @@ SETUP_IGNOREDARTICLES HE מיליות שאין להתייחס אליהן במהלך מיון IT Articoli da ignorare nell'ordinamento JA アーチスト名を並べ替える場合に無視する単語 - NL Liwoorden die bij het sorteren genegeerd moeten worden + NL Lidwoorden die bij het sorteren genegeerd moeten worden NO Artikler som skal ignoreres ved sortering PL Przedimki ignorowane przy sortowaniu PT Articos a ignorar ao ordenar nomes de artistas @@ -8097,7 +8109,7 @@ SETUP_SPLITLIST_DESC FR Le Logitech Media Server peut extraire plusieurs noms d'artistes, titres d'albums ou genres des repères ID3 de vos fichiers musicaux. Si il détecte l'un des caractères ci-dessous dans les repères ID3, il créera une entrée différente pour chaque élément. Par exemple, si le caractère point-virgule est défini comme séparateur, un fichier contenant le repère ID3 "J. S. Bach;Michael Tilson Thomas" sera listé à la fois sous "J. S. Bach" et sous "Michael Tilson Thomas". SI vous modifiez ce réglage, votre dossier de musique sera à nouveau analysé. HE Logitech Media Server יכול לחלץ מבצעים, שמות אלבומים וסגנונות מרובים מהתגים הכלולים בקובצי המוסיקה שלך. אם Logitech Media Server מאתר בתגים מילים או תווים מהרשימה שלהלן, הוא ייצור רשימות נפרדות עבור כל פריט. לדוגמה, אם אתה מציב נקודה-פסיק ברשימה שלהלן, שיר עם תג מבצע של "J. S. Bach;Michael Tilson Thomas" יופיע באזור המבצעים הן עבור "J. S. Bach" והן עבור "Michael Tilson Thomas". שינוי הגדרה זו יגרום להפעלת סריקה חוזרת של ספריית המוסיקה. IT Logitech Media Server può estrarre più artisti, titoli di album o generi dai tag inseriti all'interno dei file musicali. Se trova nei tag qualsiasi parola o carattere indicato sotto, crea un elenco separato per ognuno di essi. Per esempio, inserendo un punto e virgola nella lista sottostante, un brano che ha come tag dell'artista Gino Paoli;Ornella Vanoni, apparirà nell'elenco artisti sia come Gino Paoli che come Ornella Vanoni. Cambiando questa impostazione viene eseguita un'analisi della libreria musicale. - NL Logitech Media Server kan meerdere artiesten, albumtitels en genres uit de tags in je muziekbestanden extraheren. Als de server een van de onderstaande woorden of tekens in de tags vindt, wordt voor elk item een aparte ingang in een lijst gemaakt. Als je bijvoorbeeld een puntkomma in de onderstaande lijst zet, verschijnt een nummer met de tag 'J. S. Bach;Michael Tilson Thomas' zowel bij 'J. S. Bach' als 'Michael Tilson Thomas'. Wanneer je deze instelling wijzigt, wordt je muziekcollectie opnieuw gescand. + NL Logitech Media Server kan meerdere artiesten, albumtitels en genres uit de tags in je muziekbestanden halen. Als de server een van de onderstaande woorden of tekens in de tags vindt, wordt voor elk item een aparte ingang in een lijst gemaakt. Als je bijvoorbeeld een puntkomma in de onderstaande lijst zet, verschijnt een nummer met de tag 'J. S. Bach;Michael Tilson Thomas' zowel bij 'J. S. Bach' als 'Michael Tilson Thomas'. Wanneer je deze instelling wijzigt, wordt je muziekcollectie opnieuw gescand. NO Logitech Media Server kan hente flere artister, albumtitler og sjangere fra etikettene i musikkfilene dine. Hvis den finner noen av ordene eller tegnene nedenfor i etikettene, oppretter den separate lister for hvert element. Hvis du for eksempel skriver et semikolon i listen nedenfor, kommer en sang med artistetiketten «J. S. Bach;Michael Tilson Thomas» opp under både «J. S. Bach» og «Michael Tilson Thomas». Hvis du endrer denne innstillingen, blir musikkbiblioteket gjennomsøkt på nytt. PL Program Logitech Media Server może wyodrębnić wykonawców, tytuły albumów i gatunki ze znaczników plików muzycznych. Jeżeli program Logitech Media Server znajdzie poniższe słowa lub znaki w znacznikach, utworzy listy dla każdego elementu. Na przykład umieszczenie średnika na liście poniżej, spowoduje, że utwór ze znacznikiem wykonawcy „J. S. Bach;Michael Tilson Thomas” pojawi się w obszarze wykonawców dla „J. S. Bach” i „Michael Tilson Thomas”. Zmiana tego ustawienia spowoduje ponowne przeszukanie biblioteki muzyki. RU Logitech Media Server может извлекать из тегов музыкальных файлов имена исполнителей, названия альбомов и жанры, даже если их несколько. Если в тегах обнаружатся какие-либо из нижеперечисленных слов или символов, для каждого элемента будут созданы отдельные записи. Например, если в списке ниже поставить точку с запятой, песня с тегом исполнителя "И. С. Бах; Майкл Тилсон Томас" появится в области исполнителей и под именем "И. С. Бах", и под именем "Майкл Тилсон Томас". При изменении этой настройки будет запущено повторное сканирование медиатеки. @@ -8169,7 +8181,7 @@ SETUP_BROWSEAGELIMIT_DESC FR Spécifiez le nombre d'éléments à afficher dans la rubrique Nouveautés. Le nombre par défaut est 100. HE בחר את מספר הרצועות להצגה בעת עיון לאיתור מוסיקה חדשה. ברירת המחדל היא 100 IT Scegliere il numero di album da visualizzare quando si cerca musica nuova. L'impostazione predefinita è 100. - NL Kies het aantal tracks dat weergegeven moet worden wanneer je naar nieuwe muziek zoekt. De standaardwaarde is 100. + NL Kies het aantal nummers dat weergegeven moet worden wanneer je naar nieuwe muziek zoekt. De standaardwaarde is 100. NO Velg antall nye album som skal vises når du søker etter ny musikk. Standardverdien er 100 album. PL Wybierz liczbę najnowszych albumów podczas wyszukiwania nowej muzyki. Wartość domyślna wynosi 100. RU Выберите число последних альбомов, отображаемых при просмотре новой музыки. По умолчанию — 100 @@ -8211,7 +8223,7 @@ SETUP_SCAN_ON_PREF_CHANGE_PROMPT DE Einige Einstellungsänderungen erfordern ein erneutes Durchsuchen ihrer Mediensammlung. Klicken Sie hier, um die Sammlung neu zu durchsuchen. EN Some settings require a rescan of your library to take effect. Click here to start the scan. FR Certains paramètrages nécessitent de relancer une analyse complète de la bibliothèque. Cliquez ici pour lancer l'analyse. - NL Sommige voorkeurswijzigingen vragen om een herscan van je collectie voordat ze effect hebben. Klik hier om de scan te starten. + NL Voor sommige voorkeurswijzigingen moet je collectie opnieuw worden gescand voordat ze effect hebben. Klik hier om de scan te starten. NO Noen innstillingsendringer trer ikke i kraft før musikkbiblioteket er skannet på ny. Trykk her for å starte skanning. PL Niektóre ustawienia wymagają ponownego przeszukania biblioteki multimedialnej. Kliknij tutaj, aby uruchomić przeszukiwanie. @@ -8239,7 +8251,7 @@ SETUP_DBHIGHMEM_DESC FI Jos palvelimella on tarpeeksi muistia, voit parantaa suorituskykyä kasvattamalla tietokannan käytössä olevaa muistia. Asetuksen muuttaminen edellyttää palvelimen käynnistämistä uudelleen. FR Si votre serveur dispose d'une mémoire suffisante, vous pouvez améliorer la performance en augmentant la quantité de mémoire disponible pour la base de données. Si vous changez cette option, vous devez redémarrer le serveur. IT Se il server dispone di memoria sufficiente, è possibile ottenere prestazioni migliori incrementando la quantità di memoria disponibile per il database. Se si modifica l'opzione, sarà necessario riavviare il server. - NL Als je server voldoende geheugen heeft, kun je je prestaties verbeteren door de hoeveelheid beschikbaar geheugen voor de database te verhogen. Als je deze optie wilt wijzigen, moet je de server opnieuw opstarten. + NL Als je server voldoende geheugen heeft, kun je je prestaties verbeteren door de hoeveelheid beschikbaar geheugen voor de database te verhogen. Als je deze optie wilt wijzigen, moet je de server herstarten. NO Dersom serveren har nok minne, kan du forbedre ytelsen ved å øke mengden minne som er tilgjengelig for databasen. Du må starte serveren på nytt dersom du endrer dette alternativet. PL Jeżeli serwer ma wystarczająco dużo pamięci, można uzyskać lepszą wydajność, zwiększając ilość pamięci dostępnej dla bazy danych. Zmiana tej opcji wymaga ponownego uruchomienia serwera. RU Если на сервере достаточно памяти, можно повысить быстродействие за счет увеличения объема памяти, доступного базе данных. После изменения этого параметра необходимо перезапустить сервер. @@ -8380,7 +8392,7 @@ SETUP_LONGDATEFORMAT_DESC HE אפשרות זו קובעת את התבנית שבה נעשה שימוש עבור תאריכים ארוכים (כפי שמוצג בשעה שהנגן כבוי). נעשה שימוש במוסכמות הבאות באפשרויות המוצגות להלן:
WWWW מייצג את היום המלא בשבוע (יום שבת, יום ראשון וכו')
WWW מייצג את היום המקוצר בשבוע (שבת, ראשון וכו')
DD מייצג את היום בחודש (ללא הספרה אפס המובילה)
MM מייצג את החודש בשנה

YY מייצג את השנה בשתי ספרות
YYYY מייצג את השנה בארבע ספרות IT Questa opzione consente di impostare il formato da utilizzare per le date lunghe (come vengono visualizzate quando il lettore è spento). Nelle opzioni indicate di seguito sono utilizzate le seguenti convenzioni:
WWWW indica il giorno della settimana per esteso (lunedì, martedì e così via)
WWW indica il giorno della settimana abbreviato (lun, mar e così via)
DD indica il giorno del mese (senza zero iniziale)
MM indica il mese dell'anno
YY indica l'anno composto da due cifre
YYYY indica l'anno composto da quattro cifre JA このオプションは長い日付のフォーマットを選択します(プレーヤーがオフの状態のとき等)。
WWWW は長い曜日(月曜日、火曜日、等)を表します
WWWは短い曜日(月、火、等)を表します
DDは何日かを表します
MMは月を表します
YYは2桁の年を表します
WWWWは4桁の年を表します。 - NL Met deze optie wordt de lange datumnotatie ingesteld (zoals weergegeven wanneer het muzieksysteem uitstaat). In de onderstaande opties worden de volgende conventies gebruikt:
WWWW is de dag van de week voluit geschreven (zondag, maandag, enz.)
WWW is de afkorting van de dag van de week (zon, maa, enz.)
DD is de dag van de maand (zonder voorloopnul)
MM is de maand van het jaar
YY is het jaar in twee cijfers
YYYY is het jaar in vier cijfers + NL Met deze optie wordt de lange datumnotatie ingesteld (zoals weergegeven wanneer de muziekspeler uitstaat). In de onderstaande opties worden de volgende conventies gebruikt:
WWWW is de dag van de week voluit geschreven (zondag, maandag, enz.)
WWW is de afkorting van de dag van de week (zon, maa, enz.)
DD is de dag van de maand (zonder voorloopnul)
MM is de maand van het jaar
YY is het jaar in twee cijfers
YYYY is het jaar in vier cijfers NO Denne innstillingen angir formatet for lange datoer (vises f.eks når spilleren er av). Følgende format benyttes i alternativene nedenfor:
WWWW er ukedag (lørdag, søndag, osv.)
WWW er forkortet navn på ukedag (lør, søn, osv.)
DD er dagen i måneden (uten null foran)
MM er måneden
YY er tosifret årstall
YYYY er firesifret årstall PL Ta opcja określa format używany w długich datach (wyświetlanych na przykład po wyłączeniu odtwarzacza). W poniższych opcjach używane są następujące konwencje:
WWWW to pełny dzień tygodnia (Sobota, Niedziela itp.)
WWW to skrócony dzień tygodnia (Sob, Nie itp.)
DD to dzień miesiąca (bez początkowego zera)
MM to miesiąc roku
RR to rok dwucyfrowy
RRRR to rok czterocyfrowy PT Esta opção determina o formato longo a usar para datas (tais como a que é mostrada quando o cliente está desligado). São usadas as seguintes convenções para as opções abaixo @@ -8677,6 +8689,15 @@ SETUP_SHOWBUFFERFULLNESS SV Visa storlek för buffert ZH_CN 显示缓冲存储器的丰满度 +SETUP_CORS_ALLOWED_HOSTS + DE CORS akzeptierte Hosts + EN CORS Allowed Hosts + NL CORS toegestane hosts + +SETUP_CORS_ALLOWED_HOSTS_DESC + DE Cross-Origin Resource Sharing (CORS) ist ein Mechanismus, der Webbrowsern oder auch anderen Webclients Cross-Origin-Requests ermöglicht.

Definieren Sie eine Komma separierte Liste von Hostnamen, die auf ihren Logitech Media Server zugreifen dürfen. Es müssen Protokoll (z.B. "http"), voller Hostname ("www.example.com"), und allenfalls Port angegeben werden, wenn letzterer nicht dem Standard entspricht. In den meisten Fällen reicht ein Eintrag wie "http://www.example.com". + EN Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin.

Enter a comma separated list of hostnames you want to give access to your Logitech Media Server. The entries need to have the protocol (eg. "http"), full host name ("www.example.com"), plus the port number, should the latter be non-standard. In most cases something like "http://www.example.com" should be good enough. + SETUP_CSRFPROTECTIONLEVEL CS Úroveň ochrany CSRF DA CSRF-beskyttelsesnivau @@ -8695,21 +8716,21 @@ SETUP_CSRFPROTECTIONLEVEL ZH_CN CSRF保护程度 SETUP_CSRFPROTECTIONLEVEL_DESC - CS Jako ochranu proti útokům typu "Podvržení požadavku mezi různými stránkami (Cross Site Request Forgery - CSRF)" Logitech Media Server používá speciální kontrolu požadavků HTTP na funkce, které mohou provádět změny ve vašem systému nebo manipulovat se seznamy skladeb nebo přehrávači. Můžete si zvolit úroveň kontroly, kterou bude Logitech Media Server používat. Výchozí úroveň je Žádná. Další podrobnosti v oddíle Nápověda. - DA Som en beskyttelse mod såkaldte CSRF-sikkerhedstrusler (Cross Site Request Forgery, dvs. falske krydshenvisninger), undersøger Logitech Media Server alle HTTP-anmodninger om funktioner som kan ændre dit system eller dine afspilningslister, ekstra grundigt. Du kan selv indstille hvilket sikkerhedsniveau Logitech Media Server skal benytte. Standard er Ingen. Der er flere oplysninger i hjælpen. - DE Zum Schutz gegen 'Cross Site Request Forgery' (CSRF)-Sicherheitsrisiken prüft Logitech Media Server HTTP-Anforderungen auf Funktionen, die das System ändern oder Wiedergabelisten oder Player beeinträchtigen können. Sie können die Schutzstufe für Logitech Media Server festlegen. Standardmäßig ist kein Wert festgelegt. Weitere Informationen finden Sie in der Hilfe. - EN To protect against "Cross Site Request Forgery" (CSRF) security threats, Logitech Media Server applies special scrutiny to HTTP requests for functions that can make changes to your system or manipulate playlists or players. You may choose the level of scrutiny for Logitech Media Server to use. The default is None. See Help Section for more details. - ES Para protegerse contra amenazas CSRF ("Cross Site Request Forgery"), Logitech Media Server aplica un análisis detallado especial a las solicitudes HTTP de funciones que puedan realizar cambios en el sistema o manipular listas de reproducción o reproductores. Se puede elegir el nivel de análisis detallado que debe usar Logitech Media Server. El valor predeterminado es Ninguno. Consulte la sección de ayuda para más detalles. - FI "Cross Site Request Forgery" (CSRF) -tietoturvauhilta suojautumista varten Logitech Media Server etsii HTTP-pyynnöistä erityisen tarkasti toimintoja, jotka voivat tehdä muutoksia järjestelmään tai muokata soittoluetteloita tai soittimia. Voit valita, kuinka tarkasti Logitech Media Server tutkii pyynnöt. Oletus on Ei mitään. Katso ohjeesta lisätietoja. - FR Pour garantir une protection contre les risques de sécurité de type "Cross Site Request Forgery" (CSRF), le Logitech Media Server applique une vérification supplémentaire aux requêtes HTTP susceptibles d'altérer le système ou de manipuler des listes de lecture ou des platines. Vous pouvez spécifier le niveau de cette vérification. La valeur par défaut est Aucune. Reportez-vous à l'aide pour plus d'informations. - HE להתגוננות מפני "זיוף בקשות מאתרים צולבים" (CSRF)‏, Logitech Media Server מחיל בדיקה קפדנית מיוחדת על בקשות HTTP עבור פונקציות שיכולות לבצע שינויים במערכת או לטפל ברשימות השמעה או בנגנים. באפשרותך לבחור את רמת הבדיקה הקפדנית שבה ישתמש השרת. ברירת המחדל היא 'בינונית'. עיין במקטע העזרה לקבלת פרטים נוספים. - IT Per ottenere protezione da minacce alla sicurezza di tipo Cross Site Request Forgery (CSRF), Logitech Media Server controlla accuratamente le richieste HTTP di funzioni che possono determinare modifiche del sistema o manipolare le playlist o il lettore. È possibile scegliere il livello di controllo che il server deve adottare. Il livello predefinito è Nessuno. Per ulteriori informazioni, consultare la sezione Guida. - NL Ter bescherming tegen CSRF-beveiligingsbedreigingen (Cross Site Request Forgery) worden HTTP-verzoeken door Logitech Media Server nauwkeurig onderzocht op functies die je systeem, playlists of muzieksystemen kunnen wijzigen. Je kunt zelf instellen hoe nauwkeurig de server moet zijn. De standaardinstelling is 'Geen'. Zie de Help-sectie voor meer informatie. - NO Logitech Media Server beskytter seg mot CSRF-sikkerhetstrusler (Cross Site Request Forgery) ved å bruke en egen undersøkelse av http-forespørsler for funksjoner som kan utføre endringer på systemet eller manipulere spillelister og spillere. Du kan angi hvilket sikkerhetsnivå Logitech Media Server skal operere med. Standard er Ingen. Du kan finne ut mer i hjelp-delen. - PL W celu ochrony przed atakiem typu „CSRF” (Cross Site Request Forgery) program Logitech Media Server dokładnie bada żądania HTTP dotyczące funkcji, które mogą wprowadzić zmiany w systemie, bądź manipulować listami odtwarzania lub odtwarzaczami. Możliwe jest określenie poziomu badania używanego przez program Logitech Media Server. Ustawienie domyślne to Brak. Zapoznaj się z Pomocą, aby uzyskać szczegółowe informacje. - RU Для защиты от угроз безопасности CSRF (подделки межсайтовых запросов) HTTP-запросы в Logitech Media Server проходят специальную проверку на наличие функций, способных повлиять на работу системы, а также на плей-листы или плееры. В Logitech Media Server можно выбрать уровень проверки. По умолчанию проверка не выполняется. Дополнительные сведения см. в разделе справки. - SV För att skydda dig mot så kallade CSRF-säkerhetshot (Cross Site Request Forgery) granskas varje HTTP-begäran extra noggrant om den gäller funktioner där datorn, spelarna eller spellistorna kan manipuleras. Du kan själv välja hur noggrant granskningen ska utföras. Standardnivån är Ingen. Mer information finns i hjälpavsnittet. - ZH_CN 为了免受"CSRF"安全威胁,Logitech Media Server会向那些可能变动您的系统或操作播放表的HTTP请求特别察视。您可以选择此察视的严密程度。缺省为中等。详情请参阅帮助部分。 + CS Jako ochranu proti útokům typu "Podvržení požadavku mezi různými stránkami (Cross Site Request Forgery - CSRF)" Logitech Media Server používá speciální kontrolu požadavků HTTP na funkce, které mohou provádět změny ve vašem systému nebo manipulovat se seznamy skladeb nebo přehrávači. Můžete si zvolit úroveň kontroly, kterou bude Logitech Media Server používat. Výchozí úroveň je Žádná. Další podrobnosti v oddíle Nápověda. + DA Som en beskyttelse mod såkaldte CSRF-sikkerhedstrusler (Cross Site Request Forgery, dvs. falske krydshenvisninger), undersøger Logitech Media Server alle HTTP-anmodninger om funktioner som kan ændre dit system eller dine afspilningslister, ekstra grundigt. Du kan selv indstille hvilket sikkerhedsniveau Logitech Media Server skal benytte. Standard er Ingen. Der er flere oplysninger i hjælpen. + DE Zum Schutz gegen 'Cross Site Request Forgery' (CSRF)-Sicherheitsrisiken prüft Logitech Media Server HTTP-Anforderungen auf Funktionen, die das System ändern oder Wiedergabelisten oder Player beeinträchtigen können. Sie können die Schutzstufe für Logitech Media Server festlegen. Standardmäßig ist kein Wert festgelegt. Weitere Informationen finden Sie in der Hilfe. + EN To protect against "Cross Site Request Forgery" (CSRF) security threats, Logitech Media Server applies special scrutiny to HTTP requests for functions that can make changes to your system or manipulate playlists or players. You may choose the level of scrutiny for Logitech Media Server to use. The default is None. See Help Section for more details. + ES Para protegerse contra amenazas CSRF ("Cross Site Request Forgery"), Logitech Media Server aplica un análisis detallado especial a las solicitudes HTTP de funciones que puedan realizar cambios en el sistema o manipular listas de reproducción o reproductores. Se puede elegir el nivel de análisis detallado que debe usar Logitech Media Server. El valor predeterminado es Ninguno. Consulte la sección de ayuda para más detalles. + FI "Cross Site Request Forgery" (CSRF) -tietoturvauhilta suojautumista varten Logitech Media Server etsii HTTP-pyynnöistä erityisen tarkasti toimintoja, jotka voivat tehdä muutoksia järjestelmään tai muokata soittoluetteloita tai soittimia. Voit valita, kuinka tarkasti Logitech Media Server tutkii pyynnöt. Oletus on Ei mitään. Katso ohjeesta lisätietoja. + FR Pour garantir une protection contre les risques de sécurité de type "Cross Site Request Forgery" (CSRF), le Logitech Media Server applique une vérification supplémentaire aux requêtes HTTP susceptibles d'altérer le système ou de manipuler des listes de lecture ou des platines. Vous pouvez spécifier le niveau de cette vérification. La valeur par défaut est Aucune. Reportez-vous à l'aide pour plus d'informations. + HE להתגוננות מפני "זיוף בקשות מאתרים צולבים" (CSRF)‏, Logitech Media Server מחיל בדיקה קפדנית מיוחדת על בקשות HTTP עבור פונקציות שיכולות לבצע שינויים במערכת או לטפל ברשימות השמעה או בנגנים. באפשרותך לבחור את רמת הבדיקה הקפדנית שבה ישתמש השרת. ברירת המחדל היא 'בינונית'. עיין במקטע העזרה לקבלת פרטים נוספים. + IT Per ottenere protezione da minacce alla sicurezza di tipo Cross Site Request Forgery (CSRF), Logitech Media Server controlla accuratamente le richieste HTTP di funzioni che possono determinare modifiche del sistema o manipolare le playlist o il lettore. È possibile scegliere il livello di controllo che il server deve adottare. Il livello predefinito è Nessuno. Per ulteriori informazioni, consultare la sezione Guida. + NL Ter bescherming tegen CSRF-beveiligingsbedreigingen (Cross Site Request Forgery) worden HTTP-verzoeken door Logitech Media Server nauwkeurig onderzocht op functies die je systeem, playlists of muziekspelers kunnen wijzigen. Je kunt zelf instellen hoe nauwkeurig de server moet zijn. De standaardinstelling is 'Geen'. Zie de Help-sectie voor meer informatie. + NO Logitech Media Server beskytter seg mot CSRF-sikkerhetstrusler (Cross Site Request Forgery) ved å bruke en egen undersøkelse av http-forespørsler for funksjoner som kan utføre endringer på systemet eller manipulere spillelister og spillere. Du kan angi hvilket sikkerhetsnivå Logitech Media Server skal operere med. Standard er Ingen. Du kan finne ut mer i hjelp-delen. + PL W celu ochrony przed atakiem typu „CSRF” (Cross Site Request Forgery) program Logitech Media Server dokładnie bada żądania HTTP dotyczące funkcji, które mogą wprowadzić zmiany w systemie, bądź manipulować listami odtwarzania lub odtwarzaczami. Możliwe jest określenie poziomu badania używanego przez program Logitech Media Server. Ustawienie domyślne to Brak. Zapoznaj się z Pomocą, aby uzyskać szczegółowe informacje. + RU Для защиты от угроз безопасности CSRF (подделки межсайтовых запросов) HTTP-запросы в Logitech Media Server проходят специальную проверку на наличие функций, способных повлиять на работу системы, а также на плей-листы или плееры. В Logitech Media Server можно выбрать уровень проверки. По умолчанию проверка не выполняется. Дополнительные сведения см. в разделе справки. + SV För att skydda dig mot så kallade CSRF-säkerhetshot (Cross Site Request Forgery) granskas varje HTTP-begäran extra noggrant om den gäller funktioner där datorn, spelarna eller spellistorna kan manipuleras. Du kan själv välja hur noggrant granskningen ska utföras. Standardnivån är Ingen. Mer information finns i hjälpavsnittet. + ZH_CN 为了免受"CSRF"安全威胁,Logitech Media Server会向那些可能变动您的系统或操作播放表的HTTP请求特别察视。您可以选择此察视的严密程度。缺省为中等。详情请参阅帮助部分。 SETUP_IPFILTER_HEAD CS Blokovat příchozí spojení @@ -8944,7 +8965,7 @@ SETUP_AUTHORIZE_DESC HE באפשרותך לבחור להגן על ממשק האינטרנט של Logitech Media Server באמצעות סיסמה. בחר את האפשרות "הגנה באמצעות סיסמה" שלהלן והזן שם משתמש וסיסמה, ולאחר מכן לחץ על 'שנה'. מרגע זה ואילך, כאשר תשתמש בממשק האינטרנט של Logitech Media Server, יהיה עליך להזין את שם המשתמש והסיסמה בדפדפן האינטרנט. IT Si può scegliere di proteggere Logitech Media Server Remote Control con una password. Scegliere Protezione con password di seguito e inserire il nome utente e la password, quindi fare clic su Cambia. Da questo momento per utilizzare Logitech Media Server Remote Control è necessario inserire nel browser il nome utente e la password prescelti. JA Logitech Media Serverのウェブインターフェースをパスワードで保護することができます。下から”パスワード保護”を選んで、ユーザー名とパスワードを指定して、「変更」をクリックして下さい。以降、Logitech Media Serverのウェブインターフェースにアクセスするにはユーザー名とパスワードが必要になります。 - NL Je kunt een wachtwoord instellen voor de Logitech Media Server Remote Control. Kies hieronder 'Wachtwoordbeveiliging' en voer een gebruikersnaam en wachtwoord in. Klik dan op 'Veranderen'. Vanaf dat moment moet je deze gebruikersnaam en dit wachtwoord invoeren om toegang te krijgen tot Logitech Media Server Remote Control. + NL Je kunt een wachtwoord instellen voor de Logitech Media Server Remote Control. Kies hieronder 'Wachtwoordbeveiliging' en voer een gebruikersnaam en wachtwoord in. Klik dan op 'Wijzigen'. Vanaf dat moment moet je deze gebruikersnaam en dit wachtwoord invoeren om toegang te krijgen tot Logitech Media Server Remote Control. NO Du kan passordbeskytte nettgrensesnittet til Logitech Media Server. Velg «Passordbeskyttelse» nedenfor, skriv inn brukernavn og passord, og trykk så på Endre. Etter det vil nettgrensesnittet til Logitech Media Server be deg om brukernavn og passord. PL Program Logitech Media Server Remote Control można zabezpieczyć hasłem. Wybierz opcję „Ochrona hasłem” poniżej i wprowadź nazwę użytkownika oraz hasło, a następnie kliknij przycisk Zmień. Od tego momentu korzystanie z programu Logitech Media Server Remote Control będzie wymagało wprowadzenia tej nazwy użytkownika i hasła w przeglądarce internetowej. PT Pode activar uma palavra chave para proteger a interface web. Escolha "Proteção por Palavra Chave" abaixo, escolha um nome de utilizador e uma palavra chave, e seleccione "Mudar". A partir desse momento necessitará de introduzir o utilizador e chave para entrar na interface web com o seu browser. @@ -9260,7 +9281,7 @@ SETUP_SN_VALIDATION_FAILED FI Odottamaton virhe mysqueezebox.com-tilisi vahvistamisessa. Tarkista, että verkkoyhteys toimii. FR Une erreur inattendue est survenue lors de la validation de votre compte mysqueezebox.com. Vérifiez que la connectivité réseau fonctionne correctement. IT Si è verificato un errore imprevisto durante la convalida dell'account mysqueezebox.com. Verificare che la connessione della rete funzioni correttamente. - NL Er is een onverwachte fout opgetreden tijdens validatie van je mysqueezebox.com-account. Zorg ervoor dat de netwerkverbinding goed werkt. + NL Onverwachte fout opgetreden tijdens validatie van je mysqueezebox.com-account. Zorg ervoor dat de netwerkverbinding goed werkt. NO Det oppsto en uventet feil under opprettelse av mysqueezebox.com-kontoen. Kontroller at nettverkstilkoplingen fungerer. PL Podczas sprawdzania poprawności konta mysqueezebox.com wystąpił nieoczekiwany błąd. Upewnij się, że połączenie sieciowe działa prawidłowo. RU Непредвиденная ошибка при проверке вашей учетной записи mysqueezebox.com. Убедитесь, что сетевое подключение работает правильно. @@ -9320,7 +9341,7 @@ SETUP_SN_SYNC_DESC FI Monet soitinasetukset on synkronoitu oletuksena Logitech Media Serverin ja mysqueezebox.comin välillä. Jos haluat poistaa tämän toiminnon käytöstä, valitse alta Poista käytöstä. FR Par défaut, de nombreux réglages de platine sont synchronisés entre le Logitech Media Server et mysqueezebox.com. Pour désactiver cette fonctionnalité, sélectionnez l'option appropriée ci-dessous. IT Per impostazione predefinita numerose impostazioni del lettore in Logitech Media Server e mysqueezebox.com sono sincronizzate. Per disattivare questa caratteristica, selezionare l'opzione Disattiva in basso. - NL Standaard worden vele muzieksysteeminstellingen tussen Logitech Media Server en mysqueezebox.com gesynchroniseerd. Als je deze functie wilt uitschakelen, selecteer je de optie Uitgeschakeld hieronder. + NL Standaard worden vele muziekspelerinstellingen tussen Logitech Media Server en mysqueezebox.com gesynchroniseerd. Als je deze functie wilt uitschakelen, selecteer je de optie Uitgeschakeld hieronder. NO Som standard er mange spillerinnstillinger synkronisert mellom Logitech Media Server og mysqueezebox.com. Hvis du vil deaktivere denne funksjonen, velger du Deaktivert nedenfor. PL Domyślnie wiele ustawień odtwarzacza jest synchronizowanych między programem Logitech Media Server a usługą mysqueezebox.com. Aby wyłączyć tę funkcję, wybierz opcję Wyłączone poniżej. RU Многие настройки плеера по умолчанию синхронизованы между Logitech Media Server и mysqueezebox.com. Чтобы отключить эту возможность, выберите ниже параметр "Отключено". @@ -9335,7 +9356,7 @@ SETUP_SN_SYNC_ENABLE FI Käytössä. Soitinten asetukset pidetään synkronoituna. FR Activé. Synchroniser les réglages de platine. IT Attiva. Mantieni sincronizzate le impostazioni del lettore. - NL Ingeschakeld; muzieksysteeminstellingen gesynchroniseerd houden. + NL Ingeschakeld; muziekspelerinstellingen gesynchroniseerd houden. NO Aktivert, spillerinnstillinger synkroniseres. PL Włączone, synchronizuj ustawienia odtwarzacza RU Включено, сохранять синхронизацию настроек плеера. @@ -9350,7 +9371,7 @@ SETUP_SN_SYNC_DISABLE FI Poissa käytöstä. Soitinten asetuksia ei pidetä synkronoituna. FR Désactivé. Ne pas synchroniser les réglages de platine. IT Disattiva. Non mantenere sincronizzate le impostazioni del lettore. - NL Uitgeschakeld; muzieksysteeminstellingen niet gesynchroniseerd houden. + NL Uitgeschakeld; muziekspelerinstellingen niet gesynchroniseerd houden. NO Av, ikke synkroniser spillerinnstillinger. PL Wyłączone, nie synchronizuj ustawień odtwarzacza RU Отключено; не сохранять синхронизацию настроек плеера. @@ -9485,7 +9506,7 @@ SETUP_WIZARD_SHARE_WIN FI

Jos haluat käyttää mediatiedostoja, jotka on tallennettu toiseen tietokoneeseen tai jaettuun verkkoasemaan, varmista että jaettu asema on yhdistetty ja tiedostojärjestelmän käytettävissä. Kun asema on käytettävissä, se ilmestyy näkyviin vasemmalla olevaan valintaikkunaan. Jos tiedät käyttämäsi aseman polun, kirjoita se alle. FR

Si vous souhaitez accéder à du contenu multimédia stocké sur un autre ordinateur ou sur un lecteur partagé en réseau, vérifiez que le lecteur est monté et qu'il est disponible dans votre système de fichiers. Si le lecteur est disponible, il s'affiche dans la fenêtre de sélection de gauche. Si vous connaissez le chemin d'accès au lecteur, vous pouvez le spécifier ci-dessous. IT

Se si desidera accedere ai file multimediali memorizzati in un altro computer o in un'unità di rete condivisa, assicurarsi che l'unità condivisa sia installata e disponibile nel file system. Se l'unità è disponibile, verrà visualizzata nella parte sinistra della finestra di selezione. Inserire di seguito il percorso dell'unità a cui si desidera accedere, se disponibile. - NL

Wil je media oproepen die op een andere computer of op een gedeeld netwerkstation is opgeslagen, zorg er dan voor dat het gedeelde station geactiveerd en beschikbaar is voor je bestandssysteem. Zodra het station beschikbaar is, verschijnt het in het selectievenster aan de linkerkant. Als je het pad weet van het station dat je wilt oproepen, kun je dit hieronder invoeren. + NL

Wil je media oproepen die op een andere computer of op een gedeeld netwerkstation is opgeslagen, zorg dan dat het gedeelde station geactiveerd en beschikbaar is voor je bestandssysteem. Zodra het station beschikbaar is, verschijnt het in het selectievenster aan de linkerkant. Als je het pad weet van het station dat je wilt oproepen, kun je dit hieronder invoeren. NO

Hvis du vil ha tilgang til mediefiler som er lagret på en annen datamaskin, eller på en delt nettverksstasjon, må du sørge for at den delte stasjonen er montert og tilgjengelig på filsystemet. Så snart stasjonen er tilgjengelig, vises den i valgvinduet til venstre. Hvis du vet banen til stasjonen du vil ha tilgang til, kan du angi den nedenfor. PL

Aby uzyskać dostęp do multimediów zapisanych na innym komputerze lub udostępnionym dysku sieciowym, upewnij się, że udostępniony dysk sieciowy jest zainstalowany i dostępny w systemie plików. Po udostępnieniu dysku zostanie on wyświetlony w oknie wyboru po lewej. Jeżeli znasz ścieżkę do dysku, którego chcesz użyć, wprowadź ją poniżej. RU

Если необходимо получить доступ к файлам мультимедиа, хранящимся на другом компьютере или на общем сетевом диске, убедитесь, что общий диск подключен и доступен файловой системе. Доступные диски отображаются в окне выбора слева. Если путь к требуемому диску известен, можно ввести его ниже. @@ -9500,7 +9521,7 @@ SETUP_WIZARD_SHARE_OTHERS FI

Jos haluat käyttää mediatiedostoja, jotka on tallennettu toiseen tietokoneeseen tai jaettuun verkkoasemaan, varmista että jaettu asema on yhdistetty ja tiedostojärjestelmän käytettävissä. Kun asema on käytettävissä, se ilmestyy näkyviin vasemmalla olevaan valintaikkunaan. Jos tiedät käyttämäsi aseman polun, kirjoita se alle. FR

Si vous souhaitez accéder à du contenu multimédia stocké sur un autre ordinateur ou sur un volume partagé en réseau, vérifiez que le volume est monté et qu'il est disponible dans votre système de fichiers. Si le volume est disponible, il s'affiche dans la fenêtre de sélection de gauche. Si vous connaissez le chemin d'accès au volume, vous pouvez le spécifier ci-dessous. IT

Se si desidera accedere ai file multimediali memorizzati in un altro computer o in un volume di rete condiviso, assicurarsi che il volume condiviso sia installato e disponibile nel file system. Se il volume è disponibile, viene visualizzato nella parte sinistra della finestra di selezione. Se si conosce il percorso del volume a cui si desidera accedere, è possibile inserirlo qui sotto. - NL

Wil je media oproepen die op een andere computer of op een gedeeld netwerkvolume is opgeslagen, zorg er dan voor dat het gedeelde volume geactiveerd en beschikbaar is voor je bestandssysteem. Zodra het volume beschikbaar is, verschijnt het in het selectievenster aan de linkerkant. Als je het pad weet van het volume dat je wilt oproepen, kun je dit hieronder invoeren. + NL

Wil je media oproepen die op een andere computer of op een gedeeld netwerkvolume is opgeslagen, zorg dan dat het gedeelde volume geactiveerd en beschikbaar is voor je bestandssysteem. Zodra het volume beschikbaar is, verschijnt het in het selectievenster aan de linkerkant. Als je het pad weet van het volume dat je wilt oproepen, kun je dit hieronder invoeren. NO

Hvis du vil ha tilgang til mediefiler som er lagret på en annen datamaskin, eller på et delt dataområde på nettverket, må du sørge for at det delte dataområdet er montert og tilgjengelig på filsystemet. Så snart dataområdet er tilgjengelig, vises det i valgvinduet til venstre. Hvis du vet banen til dataområdet du vil ha tilgang til, kan du angi den nedenfor. PL

Aby uzyskać dostęp do multimediów zapisanych na innym komputerze lub udostępnionym woluminie sieciowym, upewnij się, że udostępniony wolumin jest zainstalowany i dostępny w systemie plików. Po udostępnieniu woluminu zostanie on wyświetlony w oknie wyboru po lewej. Jeżeli znasz ścieżkę do woluminu, którego chcesz użyć, wprowadź ją poniżej. RU

Если необходимо получить доступ к файлам мультимедиа, хранящимся на другом компьютере или на общем сетевом томе, убедитесь, что общий том подключен и доступен файловой системе. Доступный том отображается в окне выбора слева. Если путь к требуемому тому известен, введите его ниже. @@ -9731,7 +9752,7 @@ PLAYER HE נגן IT Lettore JA プレーヤー - NL Muzieksysteem + NL Muziekspeler NO Spiller PL Odtwarzacz PT Cliente @@ -9748,7 +9769,7 @@ PLAYERS FI Soittimet FR Platines IT Lettori - NL Muzieksystemen + NL Muziekspelers NO Spillere PL Odtwarzacze RU Плееры @@ -9765,7 +9786,7 @@ CURRENT_PLAYER HE נגן נוכחי: IT Lettore in uso: JA 現在のプレーヤー: - NL Huidig muzieksysteem: + NL Huidig muziekspeler: NO Gjeldende spiller: PL Aktualny odtwarzacz: PT Cliente Actual @@ -9782,7 +9803,7 @@ CHOOSE_PLAYER FI Valitse soitin FR Sélectionner une platine IT Scegli lettore - NL Muzieksysteem kiezen + NL Muziekspeler kiezen NO Velg spiller PL Wybierz odtwarzacz RU Выберите плеер @@ -9799,7 +9820,7 @@ NOW_PLAYING_ON HE מושמע כעת בנגן IT In riproduzione sul lettore a JA プレーヤーで現在再生中 - NL Speelt nu op het muzieksysteem van + NL Speelt nu op de muziekspeler van NO Spilles nå på PL Teraz odtwarzane na odtwarzaczu w PT A tocar no cliente Slim Player em @@ -10448,7 +10469,7 @@ SHOW_ARTWORK FI Näytä kansitaide FR Afficher les pochettes IT Mostra copertina - NL Hoesafbeeldingen weergeven + NL Albumcovers weergeven NO Vis albumomslag PL Pokaż okładkę RU Показать обложку @@ -10464,7 +10485,7 @@ SHOW_ARTWORK_SINGLE FI Näytä kansitaide FR Afficher la pochette IT Mostra copertina - NL Hoesafbeeldingen weergeven + NL Albumcovers weergeven NO Vis albumomslag PL Pokaż okładkę RU Показать обложку @@ -10479,7 +10500,7 @@ HIDE_ARTWORK FI Älä näytä kansitaidetta FR Masquer les pochettes IT Nascondi copertina - NL Hoesafbeeldingen verbergen + NL Albumcovers verbergen NO Skjul albumomslag PL Ukryj okładkę RU Скрыть обложку @@ -10862,7 +10883,7 @@ THREELINE FR Trois lignes HE שלוש שורות IT Su tre righe - NL Drieregelig + NL 3-regelig NO Trelinjet PL Linia potrójna RU Трехлинейный @@ -11086,7 +11107,7 @@ PLAYLISTDIR_IS_NOW HE באפשרותך להזין את הנתיב לספרייה שבה מאוחסנים הקבצים של רשימות ההשמעה השמורות בדיסק הקשיח. (אם אינך מעוניין לשמור רשימות השמעה, באפשרותך להשאיר שדה זה ריק.) IT È possibile specificare il percorso sul disco rigido in cui salvare i file delle playlist. Se non si desidera salvare le playlist, lasciare vuoto questo campo. JA プレイリストが保存してあるフォルダーへのパスを指定してください。(プレイリストを保存しないのであればブランクで結構です) - NL Je kunt het pad invoeren naar een map waarin je playlistbestanden op je vaste schijf moeten worden opgeslagen. (Je kunt dit leeg laten als je geen playlists wilt opslaan.) + NL Je kunt het pad invoeren naar een map waarin je playlists op je vaste schijf moeten worden opgeslagen. (Je kunt dit leeg laten als je geen playlists wilt opslaan.) NO Her kan du angi banen til katalogen der du har lagret spillelistefilene dine på harddisken. (Du kan la dette feltet stå tomt hvis du ikke vil lagre spillelister.) PL Możliwe jest wprowadzenie ścieżki do katalogu przechowywania zapisanych list odtwarzania na dysku twardym. (Pole można zostawić puste, jeżeli listy odtwarzania nie mają być zapisywane.) PT Escolha a directoria onde as playlist serão gravadas no disco. (Deixe vazio para desactivar esta opção) @@ -11265,7 +11286,7 @@ TRACK HE רצועה IT Brano JA トラック - NL Track + NL Nummer NO Spor PL Ścieżka PT Faixa @@ -11282,7 +11303,7 @@ TRACK_NUMBER FI Raidan numero FR Numéro de piste IT Numero brano - NL Tracknummer + NL Nummer NO Spornummer PL Numer utworu RU Номер дорожки @@ -11577,6 +11598,9 @@ ALC RU Apple Lossless SV Apple Lossless +ALCX + EN Apple Lossless leading audio + MOV CS Film QuickTime DA QuickTime-film @@ -11622,6 +11646,9 @@ MP4 RU MPEG-4 SV MPEG-4 +MP4X + EN MPEG-4 leading audio + SLS CS MPEG-4 SLS / HD-AAC DA MPEG-4 SLS / HD-AAC @@ -11944,6 +11971,9 @@ DSF EN DSF NO DSF +OPS + EN Opus + MODIFIED CS Upraveno DA Ændret @@ -12009,7 +12039,7 @@ COVERART HE עטיפה IT Copertine JA アートワーク - NL Hoesafbeeldingen + NL Albumcovers NO Albumomslag PL Okładka PT Capa do Álbum @@ -12265,7 +12295,7 @@ DRM HE קובץ זה נעול באמצעות ניהול זכויות דיגיטליות IT Questo file è protetto da DRM (Digital Rights Management) JA このファイルはDigital Rights Managementによりロックされています - NL Dit bestand is geblokkeerd door Digital Rights Management + NL Dit bestand is geblokkeerd door Digital Rights Management (DRM). NO Denne filen er stengt med digital rettighetsadministrasjon PL Plik jest zablokowany przez mechanizm zarządzania prawami cyfrowymi (DRM) RU Этот файл заблокирован управлением цифровыми правами @@ -12498,7 +12528,7 @@ ARTIST_SEARCH FI Artistihaku FR Recherche d'artiste IT Ricerca artista - NL Zoeken naar artiesten + NL Artiesten zoeken NO Søk etter artist PL Wyszukiwanie wykonawców RU Поиск исполнителя @@ -12869,7 +12899,7 @@ SWITCH_TO_GALLERY FI Suuri kuva FR Grandes pochettes IT Copertina grande - NL Grote hoesafbeeldingen + NL Grote albumcovers NO Stort albumomslag PL Duża okładka RU Большая обложка @@ -12899,7 +12929,7 @@ SWITCH_TO_EXTENDED_LIST FI Pieni kansikuva FR Petites pochettes IT Copertina piccola - NL Kleine hoesafbeeldingen + NL Kleine albumcovers NO Små albumomslag PL Mała okładka RU Маленькая обложка @@ -13133,7 +13163,7 @@ BROWSE_BY_ARTWORK HE עטיפה IT Copertine JA アートワークをブラウズする - NL Hoesafbeeldingen + NL Albumcovers NO Albumomslag PL Okładki RU Обложка @@ -13481,6 +13511,10 @@ MOSTPLAYED SV Mest spelade låtar ZH_CN 最常播的歌曲 +LASTPLAYED + DE Zuletzt wiedergegeben + EN Last Played + PLAYCOUNT DE Anzahl Wiedergaben EN Play Count @@ -13560,7 +13594,7 @@ SONG_INFO HE פרטי השיר IT Informazioni sul brano JA 曲の情報 - NL Informatie over nummer + NL Nummerinfo NO Sanginfo PL Informacje o utworze PT Informação da Música @@ -13626,7 +13660,7 @@ FILENAME_WARNING FI Nimessä on merkkejä, jotka eivät kelpaa. Muuta nimeä. FR Caractères non autorisés dans le nom. Veuillez le modifier IT Caratteri non validi nel nome. Correggere - NL Ongeldige tekens in naam. Verander deze. + NL Ongeldige tekens in naam. Aub wijzig. NO Ugyldige tegn i navnet, du må endre det. PL Nazwa zawiera niedozwolone znaki; zmień nazwę. RU Имя содержит недопустимые символы, измените его. @@ -13679,7 +13713,7 @@ SCAN_WARNING HE Logitech Media Server עדיין סורק את ספריית המוסיקה שלך, כך שייתכן שפריטים מסוימים טרם מופיעים באזור זה. IT L'analisi della libreria multimediale da parte di Logitech Media Server è ancora in corso. Alcune voci potrebbero non essere ancora visualizzate in questa area. JA Logitech Media Serverは、現在音楽ライブラリをスキャンしています。よって、いくつかのアイテムはまだ表示されていないかもしれません。 - NL De Logitech Media Server is je mediacollectie aan het scannen en daarom worden mogelijk nog niet alle items hier getoond. + NL De Logitech Media Server scant je mediacollectie en daarom worden mogelijk nog niet alle items hier getoond. NO Logitech Media Server søker fremdeles i mediebiblioteket, så noen elementer kan ennå mangle her. PL Program Logitech Media Server nadal przeszukuje bibliotekę multimedialną, więc niektóre elementy mogą nie być jeszcze widoczne w tym obszarze. PT O Logitech Media Server ainda está a analisar o arquivo musical, portanto é possível que não apareçam ainda alguns items nesta àrea. @@ -13697,7 +13731,7 @@ HOME_SCAN_WARNING FR Le Logitech Media Server analyse le contenu de votre bibliothèque multimédia. Veuillez patienter. HE Logitech Media Server עדיין סורק את ספריית המוסיקה שלך. אנא המתן בסבלנות; הפעולה עשויה להימשך זמן מה. IT L'analisi della libreria multimediale da parte di Logitech Media Server è ancora in corso. Attendere. L'operazione potrebbe richiedere alcuni minuti. - NL Logitech Media Server is je mediacollectie aan het scannen. Een ogenblik geduld. Dit kan even duren. + NL Logitech Media Server scant je mediacollectie. Een ogenblik geduld. Dit kan even duren. NO Logitech Media Server søker fremdeles i mediebiblioteket ditt. Vær tålmodig, da dette kan ta en stund. PL Program Logitech Media Server nadal przeszukuje bibliotekę multimedialną. Prosimy o cierpliwość - może to chwilę potrwać. RU Logitech Media Server продолжает сканирование библиотеки мультимедиа. Пожалуйста, подождите. @@ -13772,7 +13806,7 @@ PLAYER_SETTINGS HE הגדרות נגן IT Impostazioni lettore JA プレーヤーセッティング - NL Instellingen van muzieksysteem + NL Instellingen van muziekspeler NO Spillerinnstillinger PL Ustawienia odtwarzacza PT Configurações do Cliente @@ -13842,7 +13876,7 @@ JIVE_PLAYER_DISPLAY_SETTINGS FI Soittimen näyttö FR Affichage du lecteur IT Display del lettore - NL Muzieksysteemweergave + NL Muziekspeler-weergave NO Spillerskjerm PL Wyświetlacz odtwarzacza RU Экран плеера @@ -14160,7 +14194,7 @@ DEBUG_NETWORK_SQUEEZENETWORK FI Mysqueezebox.com-verkkoon kirjautuminen FR Journalisation de mysqueezebox.com IT Registrazione di mysqueezebox.com - NL Registratie van mysqueezebox.com + NL Loggen van mysqueezebox.com NO mysqueezebox.com-loggføring PL Logowanie do usługi mysqueezebox.com RU Ведение журнала mysqueesebox.com @@ -14213,7 +14247,7 @@ DEBUG_NETWORK_COMETD FI Cometd-protokollan kirjaus FR Journalisation du protocole Cometd IT Registrazione protocollo Cometd - NL Registratie van Cometd-protocol + NL Loggen van Cometd-protocol NO Cometd protokoll-loggføring PL Rejestrowanie protokołu Cometd RU Ведение журнала протокола Cometd @@ -14303,7 +14337,7 @@ DEBUG_DEFAULT FI Palauta kirjausasetukset FR Réinitialiser les préférences de journalisation IT Reimposta preferenze di registrazione - NL Registratievoorkeuren opnieuw instellen + NL Logvoorkeuren opnieuw instellen NO Tilbakestill logginnstillinger PL Resetuj preferencje rejestrowania RU Сбросить настройки ведения журнала @@ -14319,7 +14353,7 @@ SETUP_GROUP_DEBUG_DESC FR Le Logitech Media Server est doté d'un certain nombre de réglages de journalisation pouvant enregistrer des informations détaillées sur les opérations du serveur et de l'analyseur. L'enregistrement dans chaque catégorie de journalisation dépend d'un niveau de sévérité défini, le niveau "Débogage" étant le plus détaillé et "Désactivé" étant le moins détaillé. HE Logitech Media Server כולל מספר הגדרות של איתור באגים שניתן להשתמש בהן לתיעוד מידע מפורט אודות הפעלת השרת. סמן את הפריטים שברצונך לתעד ביומן לצורך הצגה ולחץ על 'שנה' כדי להתחיל לתעד את המידע ביומן. באפשרותך להציג את הרשומות האחרונות שתועדו ביומן. IT In Logitech Media Server sono disponibili numerose impostazioni per registrare informazioni dettagliate sul funzionamento del server e dell'analisi. Per ogni categoria di dati da registrare è possibile selezionare il livello di gravità: Debug definisce il livello più dettagliato mentre Disattivato quello meno dettagliato. - NL Logitech Media Server heeft een aantal registratie-instellingen waarmee gedetailleerde informatie over server- en scannerwerking verzameld kan worden. Elke categorie heeft een ernstniveau, waarbij 'Debuggen' het meest uitgebreid en 'Uit' het meest beknopt is. + NL Logitech Media Server heeft een aantal log-instellingen waarmee gedetailleerde informatie over server- en scannerwerking verzameld kan worden. Elke categorie heeft een ernstniveau, waarbij 'Debuggen' het meest uitgebreid en 'Uit' het meest beknopt is. NO Logitech Media Server har flere innstillinger for loggføring som kan brukes til å ta opp detaljert informasjon om serveren og søk. Hver loggføringskategori har en alvorlighetsgrad for loggføring. «Feilsøking» er mest detaljert, og «Av» er minst. PL Program Logitech Media Server udostępnia różne opcje rejestrowania szczegółowych informacji o działaniu programu i mechanizmu przeszukiwania. Każda kategoria rejestrowania ma kategorię ważności. Opcja „Debuguj” zapewnia najbardziej, a „Wył.” najmniej dokładne informacje. RU В Logitech Media Server предусмотрены различные настройки ведения журнала, с помощью которых можно записывать подробные сведения о работе сервера и сканера. У каждой категории ведения журнала есть уровень серьезности. Самый подробный уровень — "Отладка", наименее подробный — "Отключено". @@ -14366,7 +14400,7 @@ CHANGE HE שנה IT Cambia JA 変更 - NL Veranderen + NL Wijzigen NO Endre PL Zmień PT Mudar @@ -14401,7 +14435,7 @@ PLAYER_IP_ADDRESS HE כתובת ה-IP של הנגן היא IT L'indirizzo IP del lettore è JA このプレーヤーのIPアドレスは - NL Het IP-adres van dit muzieksysteem is + NL Het IP-adres van deze muziekspeler is NO Spillerens IP-adresse er PL Adres IP tego odtwarzacza to PT O endereço IP deste cliente é @@ -14420,7 +14454,7 @@ PLAYER_MAC_ADDRESS HE כתובת ה-MAC של האתרנט של הנגן היא IT L'indirizzo MAC Ethernet di questo lettore è JA このプレーヤーのMACアドレスは - NL Het ethernet-MAC-adres van dit muzieksysteem is + NL Het ethernet-MAC-adres van deze muziekspeler is NO Spillerens MAC-adresse er PL Adres MAC w sieci Ethernet tego odtwarzacza to PT O endereço MAC deste cliente é @@ -14498,7 +14532,7 @@ FULL_N FI Täysi kapea FR Etroit complet IT Intero stretto - NL Volledig small + NL Volledig smal NO Helt smal tekst PL Pełne wąskie RU Полный узкий @@ -14553,7 +14587,7 @@ CHOOSE_PLAYING_DISPLAY_MODE HE הנגן יכול להציג מידע אודות התקדמות השיר במהלך ההשמעה. בחר את המידע שברצונך להציג. IT Nel lettore è possibile visualizzare informazioni relative all'avanzamento del brano in corso. Scegliere le informazioni da visualizzare. JA プレーヤーは、曲の進み具合を表示することができます。表示したい情報を選んでください。 - NL Het muzieksysteem kan informatie over de voortgang van een nummer weergeven, terwijl het afgespeeld wordt. Kies de informatie die weergegeven moet worden. + NL De muziekspeler kan informatie over de voortgang van een nummer weergeven, terwijl het afgespeeld wordt. Kies de informatie die weergegeven moet worden. NO Spilleren kan vise hvor mye av sangen som er spilt av under avspillingen. Velg hvilken informasjon du vil se. PL Odtwarzacz może wyświetlić informacje o postępie odtwarzania utworu. Wybierz informacje, które chcesz wyświetlić. PT O cliente pode mostrar informações da música actual a tocar. Escolha a informação a mostrar. @@ -14718,7 +14752,7 @@ FORGET_PLAYER HE התעלמות מנגן IT Ignora lettore JA このプレーヤーを消去する - NL Muzieksysteem vergeten + NL Muziekspeler vergeten NO Glem spiller PL Nie używaj odtwarzacza PT Esquecer cliente @@ -14737,7 +14771,7 @@ FORGET_PLAYER_DESC HE אם אינך משתמש עוד בנגן זה, או אם הנגן מופיע פעמיים ברשימת הנגנים, באפשרותך לגרום לשרת להתעלם מהנגן. IT Se non si utilizza più questo lettore o se il lettore compare due volte nell'elenco dei lettori, si può fare in modo che Logitech Media Server lo ignori. JA このプレーヤーをもう使用していない場合、あるいは、このプレーヤーが2つリストに載っている場合、このプレーヤーを消去することができます。 - NL Als je dit muzieksysteem niet langer gebruikt of dit systeem twee keer voorkomt in de lijst met muzieksystemen, kun je Logitech Media Server dit systeem laten 'vergeten'. + NL Als je deze muziekspeler niet langer gebruikt of dit systeem twee keer voorkomt in de lijst met muziekspelers, kun je Logitech Media Server dit systeem laten 'vergeten'. NO Hvis du ikke bruker denne spilleren, eller hvis den vises to ganger i listen over spillere, kan du få Logitech Media Server til å glemme den. PL Jeżeli odtwarzacz nie jest już używany lub jest wyświetlany dwukrotnie na liście odtwarzaczy, program Logitech Media Server może nie używać tego odtwarzacza. PT Se não usar mais este cliente (nome ou IP), ou se o cliente aparecer várias vezes, pode "esquecê-lo". @@ -14756,7 +14790,7 @@ FORGET_PLAYER_LINK HE התעלמות מנגן זה IT Ignora questo lettore JA このプレーヤーを消去する - NL Dit muzieksysteem vergeten + NL Deze muziekspeler vergeten NO Glem denne spilleren PL Nie używaj tego odtwarzacza PT Esquecer este cliente @@ -14813,7 +14847,7 @@ CURRENT_WEB_SKIN HE מעטפת אינטרנט נוכחית: IT Skin web in uso: JA 現在のウェブ外観 - NL Huidig webuiterlijk: + NL Huidig web-skin: NO Aktivt grensesnittskall: PL Aktualna karnacja internetowa: PT Tema Web actual @@ -14845,7 +14879,7 @@ DISPLAY_PLUGINS FI Näytä Muut-valikko Slim-soittimessa: FR Afficher le menu Extras sur la platine: IT Visualizza il menu Extra nel lettore Slim: - NL Het menu Extra's op het Slim-muzieksysteem weergeven: + NL Het menu Extra's op de Slim-muziekspeler weergeven: NO Vis menyen Tillegg på Slim-spilleren: PL Wyświetl menu Dodatki w odtwarzaczu Slim: PT Mostrar menu de Extras no cliente Slim @@ -14895,7 +14929,7 @@ MUSIC_PLAYER HE נגן מוסיקה IT Lettore musicale JA プレーヤー - NL Muzieksysteem + NL Muziekspeler NO Musikkspiller PL Odtwarzacz muzyczny PT Leitor de Música @@ -14914,7 +14948,7 @@ SLIMP3_MUSIC_PLAYER HE נגן מוסיקה SLIMP3 IT Lettore musicale SLIMP3 JA SLIMP3プレーヤー - NL SLIMP3-muzieksysteem + NL SLIMP3-muziekspeler NO SLIMP3 musikkspiller PL Odtwarzacz muzyczny SLIMP3 PT Leitor de Música SLIMP3 @@ -14933,7 +14967,7 @@ SQUEEZEBOX_MUSIC_PLAYER HE נגן מוסיקה Squeezebox IT Lettore musicale Squeezebox JA Squeezeboxプレーヤー - NL Squeezebox-muzieksysteem + NL Squeezebox-muziekspeler NO Squeezebox musikkspiller PL Odtwarzacz muzyczny Squeezebox PT Leitor de Música Squeezebox @@ -14952,7 +14986,7 @@ SQUEEZEBOX2_MUSIC_PLAYER HE נגן מוסיקה Squeezebox IT Lettore musicale Squeezebox JA Squeezeboxプレーヤー - NL Squeezebox-muzieksysteem + NL Squeezebox-muziekspeler NO Squeezebox musikkspiller PL Odtwarzacz muzyczny Squeezebox PT Leitor de Música Squeezebox @@ -14971,7 +15005,7 @@ TRANSPORTER_MUSIC_PLAYER HE נגן מוסיקה Transporter IT Lettore musicale Transporter JA Transporterプレーヤー - NL Transporter-muzieksysteem + NL Transporter-muziekspeler NO Transporter musikkspiller PL Odtwarzacz muzyczny Transporter PT Leitor de Música Transporter @@ -14990,7 +15024,7 @@ SOFTSQUEEZE_MUSIC_PLAYER HE נגן מוסיקה SoftSqueeze IT Lettore musicale SoftSqueeze JA SoftSqueezeプレーヤー - NL SoftSqueeze-muzieksysteem + NL SoftSqueeze-muziekspeler NO SoftSqueeze musikkspiller PL Odtwarzacz muzyczny SoftSqueeze PT Leitor de Música SoftSqueeze @@ -15066,7 +15100,7 @@ NO_PLAYER_FOUND HE הנגן לא נמצא. IT Lettore non trovato. JA プレーヤーが見つかりません。 - NL Je muzieksysteem is niet gevonden. + NL Je muziekspeler is niet gevonden. NO Kunne ikke finne spilleren. PL Nie znaleziono odtwarzacza. PT Não foi encontrado nenhum cliente. @@ -15085,7 +15119,7 @@ NO_PLAYER_DETAILS HE אם יש ברשותך התקן Squeezebox או Transporter:

  • ודא שהנגן מחובר לחשמל ושהגדרות העבודה ברשת שלו נכונות. לאחר חיבור הנגן, לחץ על הלחצן 'רענן'.

אם אין ברשותך התקן Squeezebox או Transporter:

  • באפשרותך להאזין לספריית המוסיקה שלך באמצעות SoftSqueeze, גרסת תוכנה של Squeezebox.
  • באפשרותך להשתמש בתוכנת נגן של קובצי MP3 (כגון Winamp או iTunes) כדי להאזין לספריית המוסיקה באמצעות Logitech Media Server על-ידי התחברות לכתובת ה-URL הבאה: http://localhost:9000/stream.mp3
IT Se si possiede uno Squeezebox o un Transporter:
  • Verificare che il lettore sia collegato e che le relative impostazioni di rete siano corrette. Una volta collegato il lettore, fare clic sul pulsante Aggiorna.

Se non si possiede uno Squeezebox o un Transporter:

  • È possibile ascoltare la libreria musicale mediante SoftSqueeze, la versione software di Squeezebox.
  • È possibile ascoltare la libreria musicale in Logitech Media Server tramite software per lettori MP3 (ad esempio Winamp o iTunes) accedendo al seguente URL: http://localhost:9000/stream.mp3
JA プレーヤーがつながれ、ネットワークセッティングが正しいことを確認してください。「更新」を押して、プレーヤへの接続を再度試みてください。 - NL Als je een Squeezebox of Transporter hebt:
  • Zorg ervoor dat je muzieksysteem op het netwerk is aangesloten en dat de netwerkinstellingen kloppen. Klik op de knop Vernieuwen wanneer je systeem is aangesloten.

Als je geen Squeezebox of Transporter hebt:

  • Je kunt naar je muziekcollectie luisteren met SoftSqueeze, een softwareversie van de Squeezebox.
  • Je kunt mp3-spelersoftware (zoals Winamp of iTunes) gebruiken om met Logitech Media Server naar je muziekcollectie te luisteren. Maak hiervoor een verbinding met de volgende URL: http://localhost:9000/stream.mp3
+ NL Als je een Squeezebox of Transporter hebt:
  • Zorg ervoor dat je muziekspeler op het netwerk is aangesloten en dat de netwerkinstellingen kloppen. Klik op de knop Vernieuwen wanneer je systeem is aangesloten.

Als je geen Squeezebox of Transporter hebt:

  • Je kunt naar je muziekcollectie luisteren met SoftSqueeze, een softwareversie van de Squeezebox.
  • Je kunt mp3-spelersoftware (zoals Winamp of iTunes) gebruiken om met Logitech Media Server naar je muziekcollectie te luisteren. Maak hiervoor een verbinding met de volgende URL: http://localhost:9000/stream.mp3
NO Hvis du har en Squeezebox eller en Transporter:
  • Kontroller at spilleren er plugget inn, og at nettverksinnstillingene stemmer. Klikk på Oppdater så snart spilleren er koplet til.

Hvis du ikke har en Squeezebox eller en Transporter:

  • Du kan lytte til musikkbiblioteket ditt med SoftSqueeze, en programvareversjon av Squeezebox.
  • Du kan bruke mp3-spiller-programvare (som Winamp eller iTunes) til å lytte til musikkbiblioteket ditt med Logitech Media Server ved å gå til denne url-en: http://localhost:9000/stream.mp3
PL Jeżeli korzystasz z urządzenia Squeezebox lub Transporter:
  • Sprawdź, czy odtwarzacz jest podłączony do źródła zasilania, a ustawienia sieciowe są prawidłowe. Po podłączeniu odtwarzacza kliknij przycisk Odśwież.

Jeżeli nie korzystasz z urządzenia Squeezebox lub Transporter:

  • Możesz słuchać muzyki z biblioteki, używając programu SoftSqueeze, programowej wersji urządzenia Squeezebox.
  • W celu słuchania muzyki z biblioteki w programie Logitech Media Server możesz użyć oprogramowania odtwarzacza plików MP3 (na przykład Winamp lub iTunes), przechodząc pod następujący adres URL: http://localhost:9000/stream.mp3
PT Verifique se o cliente está ligado e se as configurações da rede estão correctas. Actualize esta página para verificar de novo a presença do leitor. @@ -15122,7 +15156,7 @@ PLAYER_NEEDS_UPGRADE_DETAILS HE יש לעדכן את תוכנת הנגן לגרסה העדכנית ביותר. לחץ ממושכות על הלחצן BRIGHTNESS (בהירות) עד שניתן יהיה לראות בתצוגת הנגן שהעדכון החל. תהליך העדכון נמשך מספר שניות בלבד. IT È necessario aggiornare il software del lettore alla versione più recente. Tenere premuto il pulsante BRIGHTNESS finché nel display del lettore non viene indicato che l'aggiornamento ha avuto inizio. L'aggiornamento richiede soltanto alcuni secondi. JA Squeezeboxのソフトウェアのアップデートが必要です。アップデートを開始したことがディスプレーに表示されるまで、リモートのBRIGHTNESSボタンを押しつづけてください。アップデートは数秒で割ります。 - NL De software van je muzieksysteem moet naar de laatste versie bijgewerkt worden. Houd de knop BRIGHTNESS ingedrukt tot op het display van het muzieksysteem wordt aangegeven dat de update is gestart. De update duurt maar een paar seconden. + NL De software van je muziekspeler moet naar de laatste versie worden bijgewerkt. Houd de knop BRIGHTNESS ingedrukt tot op het display van de muziekspeler wordt aangegeven dat de update is gestart. De update duurt maar een paar seconden. NO Programvaren i spilleren må oppdateres til siste versjon. Trykk og hold nede BRIGHTNESS-knappen på fjernkontrollen til du ser på skjermen at oppdateringen har startet. Oppdateringen tar bare noen sekunder. PL Oprogramowanie odtwarzacza wymaga zaktualizowania do najnowszej wersji. Naciśnij i przytrzymaj przycisk BRIGHTNESS, dopóki na wyświetlaczu odtwarzacza nie pojawi się komunikat o rozpoczęciu aktualizacji. Aktualizacja potrwa tylko kilka sekund. RU Необходимо обновить ПО плеера до последней версии. Нажмите и удерживайте кнопку BRIGHTNESS, пока на экране плеера не появится уведомление о начатом обновлении. Обновление займет всего несколько секунд. @@ -15176,7 +15210,7 @@ PLAYER_UPGRADE_INSTRUCTIONS HE אם כתובת ה-IP של הנגן לא מוצגת על המסך, לחץ לחיצה ממושכת על הלחצן BRIGHTNESS (בהירות) עד שתוצג. לאחר מכן, הזן כאן את כתובת ה-IP שמוצגת על המסך: IT Se l'indirizzo IP del lettore non appare nel display, tenere premuto il tasto BRIGHTNESS finché non viene visualizzato. Inserire qui l'indirizzo IP visualizzato nel display: JA もしプレーヤーのIPアドレスが画面に表示されなければ、表示されるまでBRIGHTNESSボタンを押しつづけてください。そして、ここにIPアドレスを入力してください : - NL Als het IP-adres van je muzieksysteem niet op het scherm wordt weergegeven, houd je de knop BRIGHTNESS ingedrukt tot het verschijnt. Voer dan hier het weergegeven IP-adres in: + NL Als het IP-adres van je muziekspeler niet op het scherm wordt weergegeven, houd je de knop BRIGHTNESS ingedrukt tot het verschijnt. Voer dan hier het weergegeven IP-adres in: NO Hvis IP-adressen til spilleren ikke vises på spillerens skjerm, må du holde inne knappen BRIGHTNESS til den vises. Når du ser adressen, skriver du den inn her: PL Jeżeli adres IP odtwarzacza nie jest wyświetlany na jego ekranie, naciśnij i przytrzymaj przycisk BRIGHTNESS, aż do wyświetlenia adresu. Następnie wprowadź wyświetlony adres IP widoczny na ekranie tutaj: RU Если IP-адрес плеера не отображается на экране, нажмите и удерживайте кнопку BRIGHTNESS, пока он не появится. Затем введите отображаемый IP-адрес в следующее поле: @@ -15229,7 +15263,7 @@ CHECKVERSION_PROBLEM FR Une erreur s'est produite pendant la recherche de mises à jour du Logitech Media Server: Code d'erreur %s. HE התעוררה בעיה במהלך החיפוש אחר עדכונים עבור Logitech Media Server. (קוד שגיאה %s) IT Problema durante la verifica della disponibilità di aggiornamenti per Logitech Media Server. (Codice errore %s) - NL Er is een fout opgetreden tijdens het controleren op updates voor Logitech Media Server. (Foutcode %s) + NL Fout opgetreden tijdens het controleren op updates voor Logitech Media Server. (Foutcode %s) NO Det oppsto et problem under søk etter nye oppdateringer for Logitech Media Server. (Feilkode %s) PL Podczas sprawdzania dostępności aktualizacji programu Logitech Media Server wystąpił problem (kod błędu %s). RU При поиске обновлений для Logitech Media Server возникла проблема. (Код ошибки %s) @@ -15246,7 +15280,7 @@ CHECKVERSION_ERROR FR Une erreur s'est produite pendant la recherche de mises à jour du Logitech Media Server: HE אירעה שגיאה במהלך החיפוש אחר עדכונים עבור Logitech Media Server: IT Errore durante il tentativo di aggiornamento di Logitech Media Server: - NL Er is een fout opgetreden tijdens het controleren op updates van Logitech Media Server: + NL Fout opgetreden tijdens het controleren op updates van Logitech Media Server: NO Det oppsto en feil da det ble sett etter nye oppdateringer for Logitech Media Server: PL Podczas sprawdzania dostępności aktualizacji programu Logitech Media Server wystąpił błąd: RU При проверке обновлений Logitech Media Server произошла ошибка: @@ -15283,7 +15317,7 @@ NEED_PLAYLIST_PATH HE עליך לציין נתיב אל ספרייה שבה יישמרו קובצי רשימות ההשמעה. IT Specificare un percorso per il salvataggio dei file delle playlist. JA プレイリストが保存されているフォルダーへのパスを指定しなければいけません。 - NL Geef het pad op naar de map waarin je opgeslagen playlistbestanden staan. + NL Geef het pad op naar de map waarin je opgeslagen playlists staan. NO Du må angi en bane til en katalog for lagrede spillelister. PL Należy podać ścieżkę do katalogu zapisanych plików listy odtwarzania. PT Deve especificar a directoria que contêm os ficheiros de playlist. @@ -15569,7 +15603,7 @@ SEARCHFOR_ARTIST HE איתור מבצע IT Cerca artista JA アーチストを検索する - NL Naar artiest zoeken + NL Artiest zoeken NO Søk etter artist PL Wyszukaj wykonawcę PT Procurar por Artista @@ -15588,7 +15622,7 @@ SEARCHFOR_ARTISTS HE איתור מבצעים IT Cerca artisti JA アーチストを検索する - NL Naar artiesten zoeken + NL Artiesten zoeken NO Søk etter artister PL Wyszukaj wykonawców PT Procurar por Artistas @@ -15612,7 +15646,7 @@ SEARCHFOR_ALBUM HE איתור אלבום IT Cerca album JA アルバムを検索する - NL Naar album zoeken + NL Album zoeken NO Søk etter album PL Wyszukaj album PT Procurar por Álbum @@ -15631,7 +15665,7 @@ SEARCHFOR_ALBUMS HE איתור אלבומים IT Cerca album JA アルバムを検索する - NL Naar albums zoeken + NL Albums zoeken NO Søk etter album PL Wyszukaj albumy PT Procurar por Albums @@ -15655,7 +15689,7 @@ SEARCHFOR_SONGS HE איתור שירים IT Cerca brani JA 曲を検索する - NL Naar nummers zoeken + NL Nummers zoeken NO Søk etter sanger PL Wyszukaj utwory PT Procurar por Músicas @@ -15679,7 +15713,7 @@ SEARCHFOR_SONGTITLE HE איתור שם שיר IT Cerca titolo brano JA 曲のタイトルを検索する - NL Naar titels van nummers zoeken + NL Titels van nummers zoeken NO Søk etter sangtittel PL Wyszukaj tytuł utworu PT Procurar por Música @@ -15698,7 +15732,7 @@ SEARCH_FOR_ARTISTS HE איתור מבצעים IT Cerca artisti JA アーチストを検索する - NL Naar artiesten zoeken + NL Artiesten zoeken NO Søk etter artister PL Wyszukaj wykonawców PT Procurar por Artistas @@ -15717,7 +15751,7 @@ SEARCH_FOR_ALBUMS HE איתור אלבומים IT Cerca album JA アルバムを検索する - NL Naar albums zoeken + NL Albums zoeken NO Søk etter album PL Wyszukaj albumy PT Procurar por Álbum @@ -15736,7 +15770,7 @@ SEARCH_FOR_SONGS HE איתור שירים IT Cerca brani JA 曲を検索する - NL Naar nummers zoeken + NL Nummers zoeken NO Søk etter sanger PL Wyszukaj utwory PT Procurar por Música @@ -15780,6 +15814,26 @@ SEARCHING SV Söker... ZH_CN 搜查中 +SEARCH_CONTAINS + DE enthält + EN contains + NL bevat + +SEARCH_DOESNT_CONTAIN + DE enthält nicht + EN doesn't contain + NL bevat niet + +SEARCH_STARTS_WITH + DE beginnt mit + EN starts with + NL begint met + +SEARCH_DOESNT_START_WITH + DE beginnt nicht mit + EN does not start with + NL begint niet met + FOUND CS nalezeno DA fundet @@ -15809,7 +15863,7 @@ NO_SEARCH_RESULTS FR Pas de résultat HE אין תוצאות חיפוש IT Nessun risultato trovato - NL Geen zoekresultaten + NL Niets gevonden NO Ingen søkeresultater PL Brak wyników wyszukiwania RU Ничего не найдено @@ -15827,7 +15881,7 @@ CONTRIBUTORMATCHING HE התאמת מבצע IT Artista corrispondente JA アーチスト照合 - NL Zoekresultaten voor artiesten + NL Gevonden artiest NO Treff på artist PL Dopasowywanie wykonawców PT Artistas encontrados @@ -15846,7 +15900,7 @@ CONTRIBUTORSMATCHING HE התאמת מבצעים IT Artisti corrispondenti JA アーチスト照合 - NL Zoekresultaten voor artiesten + NL Gevonden artiesten NO Treff på artister PL Dopasowywanie wykonawców PT Artistas encontrados @@ -15865,7 +15919,7 @@ ALBUMMATCHING HE התאמת אלבום IT Album corrispondente JA アルバム照合 - NL Zoekresultaten voor albums + NL Gevonden album NO Treff på album PL Dopasowywanie albumów PT Albums encontrados @@ -15884,7 +15938,7 @@ ALBUMSMATCHING HE התאמת אלבומים IT Album corrispondenti JA アルバム照合 - NL Zoekresultaten voor albums + NL Gevonden albums NO Treff på album PL Dopasowywanie albumów PT Albums encontrados @@ -15903,7 +15957,7 @@ TRACKMATCHING HE התאמת שם שיר IT Titolo corrispondente a JA 曲タイトル照合 - NL Zoekresultaten voor titels + NL Gevonden titel NO Treff på sangtittel PL Dopasowywanie tytułów utworów PT Músicas encontradas @@ -15922,7 +15976,7 @@ TRACKSMATCHING HE התאמת שמות שירים IT Brani corrispondenti JA 曲タイトル照合 - NL Zoekresultaten voor titels + NL Gevonden titels NO Treff på sanger PL Dopasowywanie utworów PT Músicas encontradas @@ -15939,7 +15993,7 @@ GENREMATCHING FI Genrejen vastaavuus FR Correspondance de genres IT Genere corrispondente a - NL Zoekresultaten voor genre + NL Gevonden genre NO Treff på sjanger PL Dopasowywanie gatunków RU Найденный жанр @@ -15954,7 +16008,7 @@ GENRESMATCHING FI Vastaavat genret FR Correspondance de genres IT Generi corrispondenti a - NL Zoekresultaten voor genres + NL Gevonden genres NO Treff på sjanger PL Dopasowywanie gatunków RU Найденные жанры @@ -15987,7 +16041,7 @@ MORE_MATCHES FR Plus de résultats... HE התאמות נוספות... IT Altri risultati... - NL Meer zoekresultaten... + NL Meer resultaten... NO Flere treff... PL Więcej pasujących elementów... RU Другие совпадения... @@ -16022,7 +16076,7 @@ SYNC_X_TO FI Synkronoi %s ja FR Synchroniser %s avec: IT Sincronizza %s in: - NL Synchronisatie %s naar: + NL Synchronisatie %s met: NO Synkroniser %s til: PL Synchronizuj %s do: RU Синхронизовать %s с: @@ -16318,6 +16372,7 @@ PATH DE Pfad EN Path FR Chemin + NL Pad RADIO_TUNEIN_NOW CS Naladit nyní @@ -16346,7 +16401,7 @@ RADIO_TUNEIN_DESC FR Si vous connaissez l'URL d'une station de radio, vous pouvez la spécifier ici et cliquer sur Connecter. Vous pouvez spécifier l'URL du flux lui-même (http://62.49.59.50:8000, par exemple) ou de la liste de lecture pointant vers le flux http://www.hitzradio.com/hitzradio.pls). HE אם ידועה לך כתובת ה-URL של התחנה שלה ברצונך להאזין, הזן אותה בשדה שלהלן ולחץ על הלחצן "התכוונן". באפשרותך להזין את כתובת ה-URL של זרימת הרדיו עצמה (לדוגמה, http://62.49.59.50:8000) או של קובץ רשימת ההשמעה שמצביע על הזרימה (לדוגמה, http://www.hitzradio.com/hitzradio.pls). IT Se si conosce già l'URL della stazione che si vuole ascoltare, inserirlo qui e fare clic sul pulsante Sintonizza. È possibile inserire l'URL dello stream della radio (esempio http://62.49.59.50:8000) o del file della playlist che punta allo stream (esempio http://www.hitzradio.com/hitzradio.pls). - NL Als je de URL al weet van de zender die je wilt beluisteren, kun je deze hieronder invoeren. Klik vervolgens op de knop Instellen. Je kunt de URL van de radio-stream zelf invoeren (bijv. http://62.49.59.50:8000), of van het playlistbestand dat naar de stream wijst (bijv. http://www.hitzradio.com/hitzradio.pls). + NL Als je de URL al weet van de zender die je wilt beluisteren, kun je deze hieronder invoeren. Klik vervolgens op de knop Instellen. Je kunt de URL van de radio-stream zelf invoeren (bijv. http://62.49.59.50:8000), of van de playlist dat naar de stream wijst (bijv. http://www.hitzradio.com/hitzradio.pls). NO Hvis du allerede vet url-en til stasjonen du vil høre på, kan du angi den her og klikke på «Finn». Du kan enten angi url-en til selve radiostrømmen (for eksempel http://62.49.59.50:8000), eller til spillelistefilen som peker til strømmen (for eksempel http://www.hitzradio.com/hitzradio.pls). PL Jeżeli znasz adres URL stacji, której chcesz posłuchać, wprowadź go tutaj, a następnie kliknij przycisk „Dostrój”. Możliwe jest wprowadzenie adresu URL strumienia audycji radiowej (np. http://62.49.59.50:8000) lub listy odtwarzania wskazującej strumień (np. http://www.hitzradio.com/hitzradio.pls). RU Если вам известен URL станции, которую вы хотите слушать, введите его здесь и нажмите кнопку "Настроить". Можно ввести URL самого радиопотока (например, http://62.49.59.50:8000) либо файла из плей-листа, указывающего на поток (например, http://www.hitzradio.com/hitzradio.pls). @@ -16562,7 +16617,7 @@ PLAYER_SETUP HE הגדרת הנגן IT Installazione lettore JA プレーヤーセットアップ - NL Set-up van muzieksysteem + NL Set-up van muziekspeler NO Spillerinnstillinger PL Konfiguracja odtwarzacza PT Configurações dos Clientes @@ -16581,7 +16636,7 @@ USING_REMOTE HE שימוש בשלט-רחוק IT Utilizzo del telecomando JA リモートを使う - NL De Slim Devices-afstandsbediening gebruiken + NL Afstandsbediening gebruiken NO Bruke fjernkontrollen PL Używanie pilota PT Como usar o Controlo Remoto @@ -16742,7 +16797,7 @@ INFORMATION_MENU_PLAYER HE מידע אודות הנגן IT Informazioni sul lettore JA プレーヤー情報 - NL Muzieksysteeminformatie + NL Muziekspeler-informatie NO Spillerinformasjon PL Informacje o odtwarzaczu RU Информация о плеере @@ -16869,7 +16924,7 @@ INFORMATION_PLAYER_MODEL HE דגם הנגן IT Modello del lettore JA プレーヤーモデル - NL Model van muzieksysteem + NL Model van muziekspeler NO Spillermodell PL Model odtwarzacza RU Модель плеера @@ -16902,7 +16957,7 @@ INFORMATION_FIRMWARE HE גרסת הקושחה של הנגן IT Versione firmware del lettore JA プレーヤーファームウェアバージョン - NL Firmwareversie van muzieksysteem + NL Firmwareversie van muziekspeler NO Spillerens fastvareversjon PL Wersja oprogramowania układowego odtwarzacza RU Версия микропрограммы плеера @@ -16935,7 +16990,7 @@ INFORMATION_PLAYER_IP HE כתובת ה-IP של הנגן IT Indirizzo IP del lettore JA プレーヤーIPアドレス - NL IP-adres van muzieksysteem + NL IP-adres van muziekspeler NO Spillerens IP-adresse PL Adres IP odtwarzacza RU IP-адрес плеера @@ -16967,7 +17022,7 @@ INFORMATION_PLAYER_PORT FR Port de la platine HE מספר היציאה של הנגן IT Porta lettore - NL Poortnummer van muzieksysteem + NL Poortnummer van muziekspeler NO Spillerens port PL Numer portu odtwarzacza RU Номер порта плеера @@ -17000,7 +17055,7 @@ INFORMATION_PLAYER_MAC HE כתובת ה-MAC של הנגן IT Indirizzo MAC del lettore JA プレーヤーMACアドレス - NL MAC-adres van muzieksysteem + NL MAC-adres van muziekspeler NO Spillerens MAC-adresse PL Adres MAC odtwarzacza RU MAC-адрес плеера @@ -17066,7 +17121,7 @@ INFORMATION_CLIENTS HE מספר כולל של נגנים שזוהו IT Totale lettori riconosciuti JA 認識されたプレーヤー数 - NL Totaalaantal herkende muzieksystemen + NL Totaalaantal herkende muziekspelers NO Totalt antall tilkoplede spillere PL Razem rozpoznanych odtwarzaczy RU Всего распознанных плееров @@ -17112,7 +17167,7 @@ INFORMATION_PLUGINDIRS FI Laajennuskansiot FR Dossiers des plugins IT Cartelle dei plugin - NL Extramappen + NL Plugin-mappen NO Plugin-mapper PL Foldery dodatków RU Папки подключаемых модулей @@ -17304,7 +17359,7 @@ INFORMATION_TRACKS HE מספר רצועות כולל IT Totale brani JA 曲数 - NL Totaalaantal tracks + NL Totaalaantal nummers NO Totalt antall spor PL Całkowita liczba utworów RU Всего дорожек @@ -17719,7 +17774,7 @@ SETUP_SCROLLPIXELS_DESC FR Sur une platine équipée d'un affichage graphique, vous pouvez ajuster le nombre de pixels (points) dont la platine avance à chaque intervalle défini précédemment. Ajustez cette valeur en même temps que la vitesse de défilement pour régler la vitesse et la qualité de défilement. Dans le doute, conservez la valeur par défaut, de 2 ou 3 pixels (min. 1, max. 20). HE בנגן עם תצוגה גרפית, באפשרותך לכוונן את מספר הפיקסלים (נקודות) שהנגן גולל בכל מרווח זמן המוגדר לעיל. כוונן ערך זה בהתאם לקצב הגלילה כדי לכוונן את מהירות וכמות הגלילה. שים לב שערכים קטנים עם קצבי גלילה נמוכים נראים טוב, אך נדרשים עבורם שרת בעל ביצועים גבוהים ורשת פנויה. אם אינך יודע במה לבחור, השאר את ערך ברירת המחדל של 7 פיקסלים. (מינימום 1, מקסימום 20). IT In un lettore con display grafico è possibile impostare il numero di pixel scorsi dal lettore in ognuno degli intervalli sopra definiti. Impostare il valore insieme alla velocità di scorrimento per regolare la velocità e la qualità dello scorrimento. In caso di dubbi, mantenere il valore predefinito di 2 o 3 pixel (minimo 1, massimo 20). - NL Op een muzieksysteem met een grafisch display kun je het aantal pixels (punten) aanpassen waarmee de tekst in elk hierboven opgegeven interval loopt. Pas dit aan in combinatie met de tekstloopsnelheid om het tempo en de kwaliteit van de tekstloop in te stellen. Houd bij twijfel de standaardwaarde van 2 of 3 pixels aan (minimaal 1, maximaal 20). + NL Op een muziekspeler met een grafisch display kun je het aantal pixels (punten) aanpassen waarmee de tekst in elk hierboven opgegeven interval loopt. Pas dit aan in combinatie met de tekstloopsnelheid om het tempo en de kwaliteit van de tekstloop in te stellen. Houd bij twijfel de standaardwaarde van 2 of 3 pixels aan (minimaal 1, maximaal 20). NO Hvis spilleren har en grafisk skjerm, kan du justere antall piksler (punkter) som spilleren ruller gjennom for hver tidsperiode som er definert ovenfor. Juster denne verdien i forhold til Rullefrekvens for å justere hastighet og kvalitet. Hvis du er usikker, bør du bruke standardverdien på 2 eller 3 piksler. (Minimumsverdi er 1, maksimum er 20.) PL W przypadku odtwarzacza z wyświetlaczem graficznym można dostosować liczbę pikseli (punktów), jaką odtwarzacz przewija w każdym okresie zdefiniowanym powyżej. Dostosuj tę opcję razem z opcją Szybkość przewijania w celu ustawienia szybkości przewijania i jakości. W razie wątpliwości pozostaw wartość domyślną wynoszącą 2 lub 3 piksele (min. 1, maks. 20). RU На плеере с графическим экраном можно выбрать число пикселей (точек), прокручиваемых плеером в течение каждого заданного выше интервала. Показатель настраивается вместе с параметром «Скорость прокрутки», при этом задается скорость прокрутки и качество. Если есть сомнения, оставьте значение по умолчанию, равное 2 или 3 пикселям. (мин. 1, макс. 20). @@ -17825,7 +17880,7 @@ SQUEEZENETWORK_CONNECTING FR Connexion à mysqueezebox.com... HE מתחבר ל-mysqueezebox.com... IT Connessione a mysqueezebox.com in corso... - NL Bezig verbinding te maken met mysqueezebox.com... + NL Verbinding te maken met mysqueezebox.com... NO Kopler til mysqueezebox.com... PL Łączenie z usługą mysqueezebox.com... RU Подключение к mysqueezebox.com... @@ -17875,7 +17930,7 @@ SQUEEZENETWORK_WANT_SWITCH FI Tämä soitin on yhdistetty mysqueezebox.comiin. Haluatko vaihtaa sen takaisin Logitech Media Serveriin? FR Cette platine est actuellement connectée à mysqueezebox.com. Voulez-vous revenir au Logitech Media Server? IT Il lettore è attualmente connesso a mysqueezebox.com. Connetterlo di nuovo a Logitech Media Server? - NL Dit muzieksysteem is momenteel met mysqueezebox.com verbonden. Wil je het systeem terugschakelen naar Logitech Media Server? + NL Deze muziekspeler is momenteel met mysqueezebox.com verbonden. Wil je het systeem terugschakelen naar Logitech Media Server? NO Spilleren er koplet til mysqueezebox.com. Vil du bytte tilbake til Logitech Media Server? PL Ten odtwarzacz jest aktualnie połączony z usługą mysqueezebox.com. Czy chcesz powrócić do programu Logitech Media Server? RU Данный плеер подключен к mysqueezebox.com. Переключиться на Logitech Media Server? @@ -17907,7 +17962,7 @@ SQUEEZENETWORK_NO_PLAYER_CONNECTED FI Jos soitinta ei ole yhdistetty, mysqueezebox.com-palveluja ei voida käyttää. FR Impossible d'accéder aux services mysqueezebox.com sans une platine connectée. IT Impossibile accedere ai servizi mysqueezebox.com se non è collegato un lettore. - NL Zonder een verbonden muzieksysteem kunnen services van mysqueezebox.com niet worden opgeroepen. + NL Zonder een verbonden muziekspeler kunnen services van mysqueezebox.com niet worden opgeroepen. NO Kan ikke bruke mysqueezebox.com-tjenester når det ikke er koplet til en spiller. PL Bez podłączenia odtwarzacza nie można uzyskać dostępu do usługi mysqueezebox.com. RU Службы mysqueezebox.com доступны только при подключенном плеере. @@ -17967,7 +18022,7 @@ MUSICSOURCE_SWITCH FI Valitse musiikkilähde, johon haluat yhdistää soittimen. FR Veuillez sélectionner la source musicale à laquelle vous souhaitez connecter la platine. IT Selezionare la sorgente musicale da connettere al lettore. - NL Selecteer de muziekbron waarmee je je muzieksysteem wilt verbinden. + NL Selecteer de muziekbron waarmee je je muziekspeler wilt verbinden. NO Velg hvilken musikkilde du vil kople spilleren til. PL Wybierz źródło muzyki, z którym chcesz połączyć odtwarzacz. RU Выберите источник музыки, к которому нужно подключить плеер. @@ -17982,7 +18037,7 @@ SQUEEZEBOX_SERVER_WANT_SWITCH FI Tämä soitin on yhdistetty kohteeseen %s. Haluatko vaihtaa sen tähän Logitech Media Serveriin? FR Cette platine est actuellement connectée à %s. Voulez-vous la connecter à ce Logitech Media Server? IT Questo lettore è connesso a %s. Passare a questo Logitech Media Server? - NL Dit muzieksysteem is momenteel verbonden met %s. Wil je het systeem terugschakelen naar deze Logitech Media Server? + NL Deze muziekspeler is momenteel verbonden met %s. Wil je het systeem terugschakelen naar deze Logitech Media Server? NO Spilleren er koplet til %s. Vil du bytte til denne Logitech Media Server? PL Odtwarzacz jest aktualnie połączony z %s. Czy chcesz przełączyć go na ten program Logitech Media Server? RU Данный плеер подключен к %s. Переключиться на данный Logitech Media Server? @@ -18185,7 +18240,7 @@ SETUP_VARIOUSARTISTSSTRING_DESC FR Lorsque des albums de compilation sont regroupés, ils s'affichent par défaut sous Artistes divers. Vous pouvez modifier ce nom ci-dessous. HE כאשר אלבומי אוסף מקובצים יחדיו, הם מופיעים תחת "מבצעים שונים" כברירת מחדל. באפשרותך לשנות את השם בשדה שלהלן. IT Quando gli album di compilation sono raggruppati, appaiono sotto Artisti vari per impostazione predefinita. È possibile cambiare la denominazione qui sotto. - NL Wanneer verzamelalbums bij elkaar gegroepeerd zijn, verschijnen ze standaard bij 'Diverse artiesten'. Je kunt die naam hieronder veranderen. + NL Wanneer verzamelalbums bij elkaar gegroepeerd zijn, verschijnen ze standaard bij 'Diverse artiesten'. Je kunt die naam hieronder wijzigen. NO Når samlealbum grupperes sammen, sorteres de under «Diverse artister» som standard. Du kan endre det navnet nedenfor. PL Po zgrupowaniu albumów zawierających kompilacje są one domyślnie wyświetlane w kategorii „Różni wykonawcy”. Nazwę tę można zmienić poniżej. RU При группировке альбомов-сборок они по умолчанию отображаются под заголовком "Различные исполнители". Это название можно изменить ниже. @@ -18219,7 +18274,7 @@ TRACKARTIST FR Artiste de la piste HE מבצע הרצועה IT Artista brano - NL Artiest van track + NL Artiest van nummer NO Sporartist PL Wykonawca utworu RU Исполнитель дорожки @@ -18285,7 +18340,7 @@ SETUP_USETPE2ASALBUMARTIST_DESC FI MP3-tagin muoto ei tarjoa standardinmukaista tapaa levyartistin määrittämiseen. Jotkin MP3-tunnistetyökalut käyttävät TPE2-kenttää levyartistille (iTunes, Winamp, Windows Media Player), joissakin toisissa työkaluissa kenttää taas saatetaan käyttää tarkoittamaan yhtyettä tai orkesteria. Valitse merkitys, jota haluat Logitech Media Serverin käyttävän. Asetuksen muuttaminen käynnistää musiikkikirjaston uudelleentarkistuksen. FR Le format d'étiquette MP3 ne fournit pas de moyen standard pour définir un artiste d'album. Certains outils d'étiquetage MP3 utilisent le champ TPE2 pour Artiste d'album (iTunes, Winamp, Windows Media Player) tandis que d'autres peuvent l'utiliser pour la signification voulue de "Groupe/Orchestre". Sélectionnez la signification que le Logitech Media Server doit utiliser. La modification de ce paramètre lancera une nouvelle analyse de votre bibliothèque musicale. IT Il formato dei tag MP3 non fornisce un metodo standard per specificare l'artista di un album. Alcuni strumenti per l'assegnazione dei tag MP3 utilizzano il campo TPE2 per specificare l'artista dell'album (iTunes, Winamp, Windows Media Player), mentre altri lo utilizzano per indicare il gruppo o l'orchestra. Selezionare il significato che si desidera utilizzare in SqueezeCenter. La modifica di questa impostazione avvierà di nuovo l'analisi della libreria musicale. - NL De mp3-tagindeling biedt geen standaardmanier om een albumartiest te definiëren. Sommige mp3-labeltools gebruiken het TPE2-veld voor Albumartiest (iTunes, Winamp, Windows Media Player) terwijl andere het gebruiken voor de beoogde betekenis van 'Band/orkest'. Selecteer de betekenis die Logitech Media Server moet gebruiken. Wanneer je deze instelling wijzigt, wordt je muziekcollectie opnieuw gescand. + NL De mp3-tagindeling biedt geen standaardmanier om een albumartiest te definiëren. Sommige mp3-tagtools gebruiken het TPE2-veld voor Albumartiest (iTunes, Winamp, Windows Media Player) terwijl andere het gebruiken voor de beoogde betekenis van 'Band/orkest'. Selecteer de betekenis die Logitech Media Server moet gebruiken. Wanneer je deze instelling wijzigt, wordt je muziekcollectie opnieuw gescand. NO Etikettformatet mp3 angir ikke en standardmåte å definere en albumartist på. Noen etikettverktøy for mp3 bruker TPE2-feltet til albumartist (iTunes, Winamp, Windows Media Player), mens andre bruker det for gruppe/orkester. Velg hvordan Logitech Media Server skal tolke dette. Når du endrer denne innstillingen, startes et nytt søk i musikkbiblioteket. PL Format znacznika pliku MP3 nie umożliwia definiowania wykonawcy albumu w standardowy sposób. Niektóre narzędzia do oznaczania plików MP3 używają do określenia wykonawcy albumu pola TPE2 (iTunes, Winamp, Windows Media Player), natomiast inne mogą używać go zgodnie z przeznaczeniem do określenia zespołu/orkiestry. Wybierz znaczenie, które ma być używane przez program Logitech Media Server. Zmiana tego ustawienia powoduje ponowne przeszukanie biblioteki muzyki. RU Формат тега MP3 не предоставляет единого стандарта для задания исполнителя альбома. В некоторых средствах для редактирования тегов MP3 поле TPE2 используется для обозначения исполнителя альбома (например, в iTunes, Winamp, проигрывателе Windows Media), тогда как в других оно может иметь подразумеваемое значение "Группа/Оркестр". Выберите необходимое значение для использования в Logitech Media Server. При изменении этой настройки начнется повторное сканирование медиатеки. @@ -18348,7 +18403,7 @@ SETUP_POWERONRESUME FR Reprise après démarrage HE חידוש ההפעלה IT Ripristino all'accensione - NL Actie bij aanzetten muzieksysteem + NL Actie bij aanzetten muziekspeler NO Fortsett ved oppstart PL Wznawianie po włączeniu zasilania RU Возобновить при вкл. @@ -18365,7 +18420,7 @@ SETUP_POWERONRESUME_DESC FR Vous pouvez choisir le type de reprise de la lecture de la liste en cours lorsque vous éteignez puis rallumez la platine. Ce réglage s'applique à cette platine et aux autres platines synchronisées. HE באפשרותך לבחור את האופן שבו הנגן מחדש את השמעת רשימת ההשמעה הנוכחית בעת לחיצה על לחצן ההפעלה/כיבוי לכיבוי הנגן ולאחר מכן פעם נוספת מאוחר יותר, להפעלתו. הגדרה זו חלה על נגן זה ועל כל יתר הנגנים המסונכרנים עם הנגן. IT È possibile scegliere il modo in cui il lettore riprende la playlist corrente quando si preme il tasto POWER per spegnere il lettore e quindi lo si preme di nuovo per riaccenderlo. Questa impostazione si applica a questo lettore e a quelli sincronizzati con esso. - NL Je kunt bepalen hoe je muzieksysteem de huidige playlist moet hervatten wanneer je het systeem uit- en later weer aanzet. Deze instelling geldt voor dit systeem en andere systemen zolang ze met dit systeem gesynchroniseerd zijn. + NL Je kunt bepalen hoe je muziekspeler de huidige playlist moet hervatten wanneer je het systeem uit- en later weer aanzet. Deze instelling geldt voor dit systeem en andere systemen zolang ze met dit systeem gesynchroniseerd zijn. NO Du kan velge hvordan spilleren skal gjenoppta spillelisten når du trykker på av/på-knappen for å slå spilleren av, og så slår den på igjen senere. Denne innstillingen gjelder denne spilleren og andre spillere som er synkronisert med denne. PL Możliwe jest wybranie sposobu wznawiania odtwarzania aktualnej listy odtwarzania po naciśnięciu przycisku POWER w celu wyłączenia, a następnie ponownego włączenia. To ustawienie dotyczy tego odtwarzacza i innych zsynchronizowanych z nim odtwarzaczy. RU Можно выбрать вариант возобновления текущего плей-листа при нажатии кнопки POWER для выключения и последующего включения плеера. Этот параметр применяется к данному плееру и другим плеерам, синхронизированным с ним. @@ -18483,7 +18538,7 @@ ARTWORK FR Pochettes HE עטיפה IT Copertine - NL Hoesafbeeldingen + NL Albumcovers NO Albumomslag PL Okładka RU Обложка @@ -18500,7 +18555,7 @@ ONE_BY_ONE_ARTWORK FR Pochette 1x1 HE עטיפות נפרדות IT Copertine una alla volta - NL Hoesafbeeldingen 1-voor-1 + NL Albumcovers 1-voor-1 NO Bla i albumomslag PL Okładka 1 na 1 RU Показ обложек по одной @@ -18550,7 +18605,7 @@ SETUP_LARGETEXTFONT_DESC FR Choisissez la police utilisée en mode texte de grande taille. HE בחר את הגופן שבו ברצונך להשתמש בעת הצגת טקסט גדול. IT Scegliere il carattere da utilizzare per testi estesi. - NL Kies het lettertype dat je wilt gebruiken, wanneer je grote tekst wilt weergeven. + NL Kies het lettertype dat je wilt gebruiken wanneer je grote tekst wilt weergeven. NO Velg hvilken skrifttype du vil bruke for stor tekst. PL Wybierz czcionkę używaną do wyświetlania dużej ilości tekstu. RU Выберите тип шрифта для отображения крупного текста. @@ -18786,7 +18841,7 @@ CLOCKSOURCE_WORD_CLOCK FR Entrée word clock HE קלט שעון טקסט IT Ingresso Word Clock - NL Word-klokinvoer + NL Word Clock-invoer NO Word clock-inndata PL Wejście zegara słów RU Вход Word Clock @@ -18993,7 +19048,7 @@ SETUP_VISUALIZERMODE_DESC FR La platine peut afficher différents mode de visualisation ou des informations texte étendues sur la droite de l'écran. Sélectionnez les éléments à afficher. Le bouton de visualisation de la platine permet de parcourir les modes de visualisation sélectionnés ci-dessous. HE הנגן יכול להציג מייצגים ויזואליים או מידע טקסט מורחב במסך הימני. בחר את הפרטים שברצונך להציג. הלחצן 'ויזואלי' בנגן יבצע מעבר בין כל האפשרויות שנבחרו להלן. IT Nella parte destra del display del lettore è possibile visualizzare informazioni estese o visualizzatori. Scegliere ciò che si desidera visualizzare. Premendo l'apposito pulsante del lettore si attivano ciclicamente le opzioni selezionate sotto. - NL Het muzieksysteem kan visualisaties of extra informatie op het rechterscherm weergeven. Kies wat er weergegeven moet worden. Met de Visual-knop op het systeem kun je de hieronder geselecteerde opties doorlopen. + NL De muziekspeler kan visualisaties of extra informatie op het rechterscherm weergeven. Kies wat er weergegeven moet worden. Met de Visual-knop op het systeem kun je de hieronder geselecteerde opties doorlopen. NO Spilleren kan vise effekter eller utvidet tekstinformasjon på høyre skjerm. Velg hvilken av disse du vil se. Du kan bla i de valgte alternativene nedenfor med knappen Visual. PL Odtwarzacz umożliwia wyświetlenie wizualizacji lub tekstu rozszerzonego na ekranie po prawej stronie. Wybierz elementy do wyświetlenia. Przycisk Wizualne w odtwarzaczu umożliwia przełączanie między opcjami wybranymi poniżej. RU Правый экран плеера может показывать визуализацию или расширенную текстовую информацию. Выберите нужный вариант отображения. С помощью кнопки "Визуальные эффекты" можно перемещаться между выбранными ниже параметрами. @@ -19008,7 +19063,7 @@ SETUP_PRECACHEARTWORK FI Levykansien tallentaminen valmiiksi välimuistiin FR Mise en pré-cache de la pochette IT Immissione copertina in pre-cache - NL Hoesafbeeldingen vooraf in cache opslaan + NL Albumcovers vooraf in cache opslaan NO Hurtigbufrer omslag på forhånd PL Wstępne buforowanie okładek RU Предварительное кэширование обложек @@ -19023,7 +19078,7 @@ SETUP_PRECACHEARTWORK_DESC FI Tarkistuksessa löydettyjen levykansien koot muutetaan ja tallennetaan välimuistiin oletuksena. Tämä parantaa suorituskykyä, kun käytetään Squeezebox-kaukosäädintä tai internet-käyttöliittymää. Tämä hidastaa tarkistusta, joten toiminto voidaan poistaa käytöstä täällä. FR Par défaut, toutes les couvertures détectées lors de la phase d'analyse sont automatiquement redimensionnées et mises en cache pour améliorer les performances lors de l'utilisation du Squeezebox Controller ou de l'interface Web. Cette fonction ralentissant le processus d'analyse, vous pouvez la désactiver ici. IT Per impostazione predefinita, tutte le copertine rilevate durante l'analisi vengono ridimensionate e inserite nella cache al fine di migliorare le prestazioni durante l'utilizzo di Squeezebox Controller o dell'interfaccia Web. Poiché questa operazione rallenta il processo di analisi, è possibile disattivarla qui. - NL De grootte van alle hoesafbeeldingen die tijdens de scanfase worden gevonden, wordt standaard aangepast en de afbeeldingen worden in cache opgeslagen om prestaties te verbeteren wanneer Squeezebox Controller of de webinterface wordt gebruikt. Aangezien het scanproces hierdoor wordt vertraagd, kun je het hier desgewenst uitschakelen. + NL Alle albumcovers die tijdens het scannen worden gevonden, worden standaard in groottecaangepast en gecachet om prestaties te verbeteren wanneer Squeezebox Controller of de webinterface wordt gebruikt. Aangezien het scanproces hierdoor wordt vertraagd, kun je het hier desgewenst uitschakelen. NO Som standard tilpasses størrelsen på albumomslag som finnes i søk, og det hurtigbufres for å forbedre ytelsen ved bruk av Squeezebox Controller eller webgrensesnittet. Dette gjør søket tregere, så derfor kan det deaktiveres her. PL Domyślnie wszystkie okładki znalezione podczas przeszukiwania są automatycznie dopasowywane pod względem rozmiaru i buforowane w celu poprawy wydajności podczas korzystania z urządzenia Squeezebox Controller lub interfejsu internetowego. Powoduje to spowolnienie procesu przeszukiwania, więc tę opcję można wyłączyć tutaj. RU По умолчанию все обложки, обнаруженные в ходе сканирования, автоматически изменяют размер и кэшируются, чтобы улучшить производительность при работе в Squeezebox Controller или веб-интерфейсе. Поскольку данная функция может замедлить сканирование, ее можно отключить. @@ -19038,7 +19093,7 @@ SETUP_PRECACHEARTWORK_ALL FI Tallentaa levynkannet, valokuvat ja videot valmiiksi välimuistiin FR Mettre la pochette de l'album, les photos et les vidéos en pré-cache IT Inserire le copertine di album, foto e video nella pre-cache - NL Afbeeldingen van albums, foto's en video's vooraf in cache opslaan + NL Albumcovers, foto's en video's vooraf in cache opslaan NO Forhåndsbufre album-, foto- og videoomslag PL Buforuj wstępnie materiały graficzne do albumów, zdjęć i filmów wideo RU Pre-cache album, photo and video artwork @@ -19053,7 +19108,7 @@ SETUP_PRECACHEARTWORK_ENABLED FI Tallenna levykannet valmiiksi välimuistiin FR Mettre la pochette de l'album en pré-cache IT Copertina album in pre-cache - NL Afbeeldingen van albums vooraf in cache opslaan + NL Albumcovers vooraf in cache opslaan NO Forhåndsbufre albumomslag PL Buforuj wstępnie okładkę albumu RU Предварительно кэшировать обложку альбома @@ -19068,7 +19123,7 @@ SETUP_PRECACHEARTWORK_DISABLED FI Älä tallenna levykansia valmiiksi välimuistiin FR Ne pas mettre la pochette en pré-cache IT Non inserire le copertine nella pre-cache - NL Hoesafbeeldingen niet vooraf in cache opslaan + NL Albumcovers niet vooraf in cache opslaan NO Ikke hurtigbufre omslag på forhånd PL Nie buforuj wstępnie okładek RU Не кэшировать обложки предварительно @@ -19086,7 +19141,7 @@ SETUP_IMAGEPROXY DE Plattenhüllen Grössenanpassung EN Artwork resizing FR Redimensionner les pochettes - NL Afbeeldingen schalen + NL Albumcovers schalen NO Skalering av plateomslag PL Dopasowywanie rozmiarów okładek @@ -19094,7 +19149,7 @@ SETUP_IMAGEPROXY_DESC DE Bevor Plattenhüllen zu den Endgeräten geschickt werden, werden sie in der Grösse angepasst, um eine gute Qualität und Performance zu erreichen. Dies erfolgt standardmässig auf mysqueezebox.com. Sollten Sie eine interne Quelle verwenden, welche von mysqueezebox.com nicht erreicht werden kann, oder möchten Sie unabhängiger von mysqueezebox.com sein, so verwenden Sie die Logitech Media Server gestützte Grössenanpassung. EN In order to get best quality artwork and performance on the devices, artwork files are resized before being sent to the client. By default this is handled by mysqueezebox.com. If you want to be independent from mysqueezebox.com or are running an internal streaming source, you might want to use Logitech Media Server's internal resizing. FR Pour obtenir des pochettes de la meilleure qualité et un affichage performant, les pochettes doivent être redimensionnées au préalabale sur le serveur, mysqueezebox.com par défaut. Pour être indépendant de mysqueezebox.com ou pour diffuser des sources locales, vous pouvez choisir Logitech Media Server pour redimensionner les pochettes. - NL Om de beste kwaliteit afbeeldingen en prestatie van je systeem te krijgen, worden deze geschaald voordat ze naar je muzieksysteem worden gestuurd. Standaard wordt dit door mysqueezebox.com gedaan. Als je onafhankelijk van mysqueezebox.com wilt zijn of een interne muziekbron gebruikt, kun je Logitech Media Server gebruiken om te schalen. + NL Om de beste kwaliteit albumcovers en prestatie van je systeem te krijgen, worden deze geschaald voordat ze naar je muziekspeler worden gestuurd. Standaard wordt dit door mysqueezebox.com gedaan. Als je onafhankelijk van mysqueezebox.com wilt zijn of een interne muziekbron gebruikt, kun je Logitech Media Server gebruiken om te schalen. NO For å få høyest mulig billedkvalitet og ytelse på avspillerene, blir omslagsbilder skalert før avspillerene får dem. Som standard er det mysqueezebox.com som tar seg av dette, men hvis du vil være uavhengig av mysqueezebox.com eller kjører en intern strømmekilde, bør du vurdere å bruke Logitech Media Servers innebygde skaleringsfunksjon. PL W celu uzyskania najlepszej jakości okładki i wydajności na odtwarzaczach, okładki są dopasowywane względem rozmiaru przed przesłaniem do odtwarzacza. Domyślnie jest to obsłużone przez mysqueezebox.com. Jeśli chcesz być niezależny od mysqueezebox.com, lub używasz lokalnego źródła, możesz pozwolić, aby to Logitech Media Server zajął się dopasowywaniem okładek. @@ -19102,7 +19157,7 @@ SETUP_IMAGEPROXY_REMOTE DE Plattenhüllen auf mysqueezebox.com anpassen EN Use mysqueezebox.com to resize artwork FR Redimensionner les pochettes avec mysqueezebox.com - NL Gebruik mysqueezebox.com om afbeeldingen te schalen + NL Gebruik mysqueezebox.com om albumcovers te schalen NO La mysqueezebox.com skalere plateomslag PL Użyj mysqueezebox.com do dopasowywania okładek @@ -19110,7 +19165,7 @@ SETUP_IMAGEPROXY_LOCAL DE Plattenhüllen lokal in Logitech Media Server anpassen EN Use Logitech Media Server to resize artwork FR Redimensionner les pochettes avec Logitech Media Server - NL Gebruik Logitech Media Server om afbeeldingen te schalen + NL Gebruik Logitech Media Server om albumcovers te schalen NO La Logitech Media Server skalere plateomslag PL Użyj Logitech Media Server do dopasowywania okładek @@ -19430,7 +19485,7 @@ SETUP_WORDCLOCKOUTPUT FR Word Clock sur les sorties S/PDIF HE שעון טקסט ברכיבי פלט מסוג S/PDIF IT Wordclock su uscite S/PDIF: - NL Word-klok op S/PDIF-uitgangen + NL Word Clock op S/PDIF-uitgangen NO Word Clock for S/PDIF-utganger PL Zegar słów na wyjściach S/PDIF RU Word Clock на выходах S/PDIF @@ -19461,7 +19516,7 @@ WORDCLOCKOUTPUT_DISABLED FI Word Clock -lähtö ei ole käytössä (lähde on päälähtö) FR La sortie word clock est désactivée (la source est le dispositif maître) IT Output di Word Clock disattivato (la sorgente è il dispositivo principale) - NL Woordklokuitvoer is uitgeschakeld (Bron is master) + NL Word Clock-uitgang is uitgeschakeld (Bron is master) NO Word clock-utdata deaktivert (kilde er master) PL Wyjście zegara słów jest wyłączone (źródło jest główne) RU Выход Word Clock отключен (источник — ведущий) @@ -19476,7 +19531,7 @@ WORDCLOCKOUTPUT_ENABLED FI Word Clock -lähtö on käytössä (Transporter on päälaite) FR La sortie word clock est activée (Transporter est le dispositif maître) IT Output Word Clock è attivato (Transporter è il dispositivo principale) - NL Woordklokuitvoer ingeschakeld (Transporter is master) + NL Word Clock-uitgang ingeschakeld (Transporter is master) NO Word clock-utdata aktivert (Transporter er master) PL Wyświetlanie zegara słów jest włączone (urządzenie Transporter jest główne) RU Выход Word clock включен (Transporter — ведущее устройство) @@ -19569,7 +19624,7 @@ SETUP_POWEROFFDAC_DESC FR Lorsque Transporter est désactivé, il est possible de désactiver entièrement la section audio. Cette désactivation peut entraîner un effet de bruit. HE כאשר Transporter כבוי, ניתן לכבות לחלוטין את מקטע השמע. כיבוי מקטע השמע עלול לגרום להופעת צליל נקישה. IT Quando Transporter è disattivato, è possibile disattivare completamente l'audio. Questa operazione potrebbe dare luogo a uno schiocco. - NL Wanneer de Transporter uitgeschakeld wordt, kan het audiogedeelte helemaal uitgezet worden. Je kunt dan een pop-geluid horen. + NL Wanneer de Transporter uitgeschakeld wordt, kan het audiogedeelte helemaal worden uitgezet. Je kunt dan een pop-geluid horen. NO Når Transporter er avslått, kan lyddelen slås helt av. Dette kan føre til en hørbar poppelyd. PL Po wyłączeniu urządzenia Transporter zasilanie części audio może zostać całkowicie odłączone. Odłączenie części audio może spowodować emisję słyszalnego pojedynczego dźwięku. RU При отключенном устройстве Transporter можно полностью выключить аудиосекцию. При ее выключении может быть слышен треск. @@ -19637,7 +19692,7 @@ SETUP_DISABLEDAC_DESC FR Lorsque la Squeezebox est désactivée, le convertisseur de signal analogique à numérique et les sorties numériques peuvent être désactivés. La désactivation des sorties peut entraîner des effets de bruit sur certains récepteurs. HE נגן ה-Squeezebox כיבה את הממיר מדיגיטלי-לאנלוגי וניתן להשבית את הפלט הדיגיטלי. השבתת הפלט עלולה לגרום להופעת צלילי נקישות או רעשים במקלטים מסוימים. IT Quando Squeezebox è spento è possibile disattivare il convertitore da digitale ad analogico e le uscite digitali. In alcuni ricevitori, la disattivazione di tali uscite potrebbe provocare uno schiocco o un disturbo. - NL Wanneer de Squeezebox uitstaat, kun je de digitaal-naar-analoog-omzetter en de digitale uitgangen uitschakelen. Bij sommige ontvangers hoor je een pop-geluid of ruis, wanneer de uitgangen uitgeschakeld worden. + NL Wanneer de Squeezebox uitstaat, kun je de digitaal-naar-analoog-omzetter (DAC) en de digitale uitgangen uitschakelen. Bij sommige ontvangers hoor je een pop-geluid of ruis, wanneer de uitgangen uitgeschakeld worden. NO Når Squeezebox er avslått, kan du deaktivere D/A-omformeren og digitale utenheter. Når du deaktiverer utenhetene kan du høre et smekk eller annen støy i enkelte mottakere. PL Po wyłączeniu urządzenia Squeezebox można deaktywować konwerter analogowo-cyfrowy i wyjścia cyfrowe. Wyłączenie wyjść może spowodować emisję słyszalnego pojedynczego dźwięku lub szumu. RU Когда Squeezebox выключен, цифро-аналоговый преобразователь и цифровые выходы можно отключить. При отключении выходов в некоторых приемниках может раздаваться треск или шум. @@ -19702,7 +19757,7 @@ SETUP_ANALOGOUTMODE_DESC FI Valitse Boomin lähtöportin toiminto (kuulokkeet/subwoofer). Jos valitset kuulokkeet, sisäiset kaiuttimet mykistyvät. "Aina käytössä" pitää sisäisen kaiuttimen käytössä silloinkin, kun kuulokkeet on liitetty. FR Sélectionnez la fonction du port de sortie du casque/sub Boom. L'option Toujours activé garde les haut-parleurs internes activés, même si un casque est branché. IT Scegliere la funzione della porta di uscita per cuffia/subwoofer del radioregistratore. L'opzione Cuffia disattiva gli altoparlanti interni. Con l'opzione Sempre attivato, gli altoparlanti interni saranno sempre attivati, anche quando sono collegate le cuffie. - NL Kies de functie van de koptelefoon-/subwooferuitgang voor de Boom. Met de koptelefoonoptie worden de interne speakers uitgeschakeld. Met 'Altijd aan' blijven de interne speakers ingeschakeld, zelfs wanneer er een koptelefoon is aangesloten. + NL Kies de functie van de koptelefoon-/subwooferuitgang voor de Boom. Met de koptelefoonoptie schakelt de interne speakers uit. Met 'Altijd aan' blijven de interne speakers ingeschakeld, zelfs wanneer er een koptelefoon is aangesloten. NO Velg funksjonen til utgangen for hodetelefoner/basshøyttaler på Boom. Hvis du velger hodetelefoner, slås de interne høyttalerne av. Hvis du velger "Alltid på", beholdes de interne høyttalerne på, selv om hodetelefonene er koplet til. PL Wybierz funkcję gniazda słuchawek/wyjścia subwoofera. Włączenie słuchawek spowoduje wyłączenie głośników wewnętrznych. Włączenie opcji „Zawsze włączone” spowoduje, że głośniki wewnętrzne będą włączone nawet po podłączeniu słuchawek. RU Выберите в устройстве Boom функцию порта выхода на наушники/сабвуфер. При установке параметра "Наушники" внутренние динамики отключаются. При установке параметра "Всегда включено" внутренние динамики остаются включенными даже при подключенных наушниках. @@ -19897,7 +19952,7 @@ LOADING FR Chargement... HE טוען... IT Caricamento... - NL Bezig met laden... + NL Laden... NO Laster... PL Ładowanie... RU Загрузка... @@ -19930,7 +19985,7 @@ DEBUG_ARTWORK FI Kansitaiteen näyttö ja vastaavuus FR Affichage et correspondance de pochettes IT Associazione e visualizzazione copertine - NL Weergave en zoekresultaten voor hoesafbeeldingen + NL Albumcovers Weergave en Gevonden NO Visning og bruk av albumomslag PL Wyświetlanie i dopasowywanie okładek RU Показ и сопоставление обложек @@ -19940,7 +19995,7 @@ DEBUG_ARTWORK_IMAGEPROXY DE Plattenhüllen Grössenanpassunsproxy EN Artwork Resizing Proxy FR Redimensionner les pochettes en amont - NL Afbeeldingen Schalen Proxy + NL Albumcovers Schalen Proxy NO Proxy for skalering av plateomslag PL Proxy dla dopasowywania rozmiarów okładek @@ -20028,7 +20083,7 @@ DEBUG_SERVER_SCHEDULER FI Sisäisen ajoituksen kirjaus FR Journalisation du planificateur interne IT Registrazione pianificazione interna - NL Registratie van interne planner + NL Loggen van interne planner NO Loggføring av internplanlegging PL Rejestrowanie wewnętrznego harmonogramu RU Ведение журнала внутреннего планировщика @@ -20058,7 +20113,7 @@ DEBUG_SERVER_SELECT FI Sisäisen valintasilmukan kirjaus (kokeneet käyttäjät) FR Journalisation de la boucle de sélection interne (avancé) IT Registrazione ciclo di selezione interno (per utenti esperti) - NL Registratie van interne selectie-loop (Geavanceerd) + NL Loggen van interne selectie-loop (Geavanceerd) NO Loggføring av intern select-spørringssløyfe (avansert) PL Rejestrowanie wewnętrznej pętli wyboru (zaawansowane) RU Ведение журнала внутренней команды "Выбрать петли" (дополнительно) @@ -20103,7 +20158,7 @@ DEBUG_NETWORK_HTTP FI Sisäisen HTTP-palvelimen kirjaus FR Journalisation du serveur http interne IT Registrazione server HTTP interno - NL Registratie van interne HTTP-server + NL Loggen van interne HTTP-server NO Intern http-serverloggføring PL Rejestrowanie wewnętrznego serwera HTTP RU Ведение журнала внутреннего HTTP-сервера @@ -20133,7 +20188,7 @@ DEBUG_NETWORK_ASYNCDNS FI Epäsynkroninen DNS-kirjaus FR Journalisation DNS asynchrone IT Registrazione DNS asincrona - NL Registratie van asynchroon DNS + NL Loggen van asynchroon DNS NO Usynkron DNS-loggføring PL Rejestrowanie asynchronicznego DNS RU Ведение журнала асинхронной DNS @@ -20148,7 +20203,7 @@ DEBUG_NETWORK_PROTOCOL FI Kaikkien soittimien protokollien kirjaus FR Journalisation du protocole de toutes les platines IT Registrazione dei protocolli di tutti i lettori - NL Registratie van protocols voor alle muzieksystemen + NL Loggen van protocols voor alle muziekspelers NO Loggføring av alle spillerprotokoller PL Rejestrowanie protokołu wszystkich odtwarzaczy RU Ведение журнала протоколов всех плееров @@ -20208,7 +20263,7 @@ DEBUG_NETWORK_JSONRPC FI JSON-RPC API -kirjaus FR Journalisation API JSON-RPC IT Registrazione API JSON-RPC - NL Registratie van JSON-RPC API + NL Loggen van JSON-RPC API NO Loggføring av JSON-RPC API PL Rejestrowanie JSON-RPC API RU Ведение журнала API JSON-RPC @@ -20313,7 +20368,7 @@ DEBUG_DATABASE_INFO FI Metatietojen ja jäsennyksen kirjaus FR Journalisation des métadonnées et de l'analyse IT Registrazione metadati e analisi - NL Registratie van metagegevens en parsering + NL Loggen van metagegevens en parsering NO Loggføring av metadata og analyse PL Rejestrowanie metadanych i analizy RU Ведение журнала метаданных и анализа @@ -20366,7 +20421,7 @@ DEBUG_CONTROL_STDIO FI Standardien I/O-komentojen kirjaus (kokeneet käyttäjät) FR Journalisation de commande d'E/S standard (avancé) IT Registrazione comandi I/O standard (per utenti esperti) - NL Registratie van standaard-I/O-opdrachten (Geavanceerd) + NL Loggen van standaard-I/O-opdrachten (Geavanceerd) NO Loggføring av standard I/O-kommandoer (avansert) PL Rejestrowanie standardowych poleceń We/Wy (zaawansowane) RU Ведение журнала стандартных команд ввода-вывода (дополнительно) @@ -20381,7 +20436,7 @@ DEBUG_CONTROL_QUERIES FI Hallintakysymysten kirjaus (kokeneet käyttäjät) FR Journalisation des requêtes de contrôle (avancé) IT Registrazione query di controllo (per utenti esperti) - NL Queryregistratie controleren (Geavanceerd) + NL Query-log controleren (Geavanceerd) NO Loggføring av kontrollforespørsler (avansert) PL Kontroluj rejestrowanie zapytań (zaawansowane) RU Ведение журнала контрольных запросов (дополнительно) @@ -20396,7 +20451,7 @@ DEBUG_CONTROL_COMMAND FI Sisäisen komennon suorituksen kirjaus (kokeneet käyttäjät) FR Journalisation d'exécution de commandes internes (avancé) IT Registrazione esecuzione comandi interni (per utenti esperti) - NL Registratie van interne opdrachtuitvoering (Geavanceerd) + NL Loggen van interne opdrachtuitvoering (Geavanceerd) NO Loggføring av utføring av interne kommandoer (avansert) PL Rejestrowanie wykonania poleceń zewnętrznych (zaawansowane) RU Ведение журнала выполнения внутренних команд (дополнительно) @@ -20411,7 +20466,7 @@ DEBUG_PLAYER_ALARMCLOCK FI Soittimen herätyskellon kirjaus FR Journalisation du réveil de la platine IT Registrazione sveglia lettore - NL Wekkerregistratie muzieksysteem + NL Loggen van wekker muziekspeler NO Loggføring av spillerens vekkerklokke PL Rejestrowanie budzika odtwarzacza RU Ведение журнала будильника плеера @@ -20441,7 +20496,7 @@ DEBUG_PLAYER_MENU FI Soitinvalikon kirjaaminen FR Journalisation du menu de la platine IT Registrazione del menu del lettore - NL Registratie van muzieksysteemmenu + NL Loggen van muziekspeler-menu NO Loggføring av spillermeny PL Rejestrowanie menu odtwarzacza RU Ведение журнала меню плеера @@ -20456,12 +20511,27 @@ DEBUG_PLAYER_UI FI Soittimen käyttöliittymän tiedot FR Informations de l'interface utilisateur de la platine IT Informazioni sull'interfaccia utente del lettore - NL Gegevens van muzieksysteeminterface + NL Gegevens van muziekspelerinterface NO Informasjon om spillerens brukergrensesnitt PL Informacje o interfejsie użytkownika odtwarzacza RU Информация о пользовательском интерфейсе плеера SV Information om användargränssnitt för spelaren +DEBUG_PLAYER_UI_SCREENSAVER + CS Protokolování spořič obrazovky + DA Logføring af pauseskærm + DE Bildschirmschoner-Protokollierung + EN Player Screensaver Logging + ES Registro de protector de pantalla + FI Näytönsäästäjänä kirjaaminen + FR Journalisation d'écran de veille + IT Registrazione del salvaschermo del lettore + NL Loggen van schermbeveiliger + NO Loggføring av spiller skjermsparer + PL Rejestrowanie wygaszacz ekranu + RU Ведение журнала экранной заставка + SV Loggning för skärmsläckare + DEBUG_PLAYER_DISPLAY CS Informace na displeji přehrávače DA Oplysninger om afspillerens display @@ -20471,7 +20541,7 @@ DEBUG_PLAYER_DISPLAY FI Soittimen näyttötiedot FR Informations de l'affichage de la platine IT Informazioni sul display del lettore - NL Displaygegevens van muzieksysteem + NL Displaygegevens van muziekspeler NO Informasjon på spillerens skjerm PL Informacje na wyświetlaczu odtwarzacza RU Информация об экране плеера @@ -20486,7 +20556,7 @@ DEBUG_PLAYER_FONTS FI Soittimen näyttö (fontti- ja bittikarttatiedot) FR Affichage de la platine (informations police et bitmap) IT Display del lettore (informazioni su caratteri e bitmap) - NL Muzieksysteemdisplay (Lettertype- en bitmapinformatie) + NL Muziekspeler-display (Lettertype- en bitmapinformatie) NO Spillerskjerm (informasjon om skrifttype og punktgrafikk) PL Wyświetlacz odtwarzacza (informacje o czcionkach i mapach bitowych) RU Экран плеера (данные о шрифтах и растровых изображениях) @@ -20501,7 +20571,7 @@ DEBUG_PLAYER_TEXT FI Soittimen näyttö (merkkien näyttämistä koskevat tiedot) FR Affichage de la platine (informations sur l'affichage des caractères) IT Display del lettore (informazioni sulla visualizzazione dei caratteri) - NL Muzieksysteemdisplay (Grafisch-displayinformatie) + NL Muziekspeler-display (Grafisch-displayinformatie) NO Spillerskjerm (informasjon om tegnvisning) PL Wyświetlanie odtwarzacza (informacje o wyświetlaniu znaków) RU Экран плеера (сведения о символьном экране) @@ -20516,7 +20586,7 @@ DEBUG_PLAYER_PLAYLIST FI Soittimen soittoluettelon ylätason hallinnan tiedot FR Informations de contrôle de liste de lecture de la platine de niveau supérieur IT Informazioni di controllo generali sulla playlist del lettore - NL Bedieningsinformatie van muzieksysteemplaylists op hoofdniveau + NL Informatie van playlists muziekspeler op hoofdniveau NO Kontrollinformasjon på øverste nivå for spiller/spilleliste PL Informacje o sterowaniu listą odtwarzania na wysokim poziomie RU Общая информация об управлении плей-листом плеера @@ -20531,7 +20601,7 @@ DEBUG_PLAYER_STREAMING FI Kaikkien soitinten virtojen kirjaus FR Journalisation de la diffusion de toutes les platines IT Registrazione di tutto lo streaming del lettore - NL Registratie van streaming voor alle muzieksystemen + NL Loggen van streaming voor alle muziekspelers NO Loggføring av all strømming til spillere PL Rejestrowanie strumieni wszystkich odtwarzaczy RU Ведение журнала потоковой передачи всех плееров @@ -20546,7 +20616,7 @@ DEBUG_PLAYER_STREAMING_DIRECT FI Soittimen suoran virtautuksen kirjaus FR Journalisation de la diffusion directe de la platine IT Registrazione dello streaming diretto del lettore - NL Registratie van directe streaming voor muzieksysteem + NL Muziekspeler: directe streaming NO Loggføring av direktestrømming til spiller PL Rejestrowanie bezpośredniego przesyłania strumieniowego przez odtwarzacz RU Ведение журнала прямой потоковой передачи плеера @@ -20561,7 +20631,7 @@ DEBUG_PLAYER_STREAMING_REMOTE FI Soittimen etävirtautuksen kirjaus FR Journalisation de la diffusion à distance de la platine IT Registrazione dello streaming remoto del lettore - NL Registratie van streaming op afstand voor muzieksysteem + NL Muziekspeler: streaming op afstand NO Loggføring av ekstern strømming til spiller PL Rejestrowanie zdalnego przesyłania strumieniowego przez odtwarzacz RU Ведение журнала удаленной потоковой передачи плеера @@ -20576,7 +20646,7 @@ DEBUG_PLAYER_FIRMWARE FI Soittimen laitteisto-ohjelmiston päivityksen kirjaus FR Journalisation de la mise à niveau du micrologiciel de la platine IT Informazioni sull'aggiornamento del firmware del lettore - NL Registratie van firmware-upgrade voor muzieksysteem + NL Muziekspeler: firmware-upgrade NO Loggføring av oppgradering av spillerens fastvare PL Rejestrowanie uaktualniania oprogramowania układowego odtwarzacza RU Ведение журнала обновления микропрограммы плеера @@ -20591,7 +20661,7 @@ DEBUG_PLAYER_SOURCE FI Soittimen lähdeäänen ja muunnoksen kirjaus FR Journalisation de la source audio et de la conversion source de la platine IT Registrazione conversione e sorgente audio del lettore - NL Registratie van bronaudio en -conversie van muzieksysteem + NL Muziekspeler: bronaudio en -conversie NO Loggføring av lyd fra spillerkilde og konvertering PL Rejestrowanie dźwięku i konwersji źródła odtwarzacza RU Ведение журнала источника аудио и преобразования плеера @@ -20606,7 +20676,7 @@ DEBUG_PLAYER_SYNC FI Usean soittimen synkronoinnin kirjaus FR Journalisation de la synchronisation des platines multiples IT Registrazione della sincronizzazione di più lettori - NL Synchronisatieregistratie van meerdere muzieksystemen + NL Loggen van synchronisatie van meerdere muziekspelers NO Loggføring av synkronisering mellom flere spillere PL Rejestrowanie synchronizacji wielu odtwarzaczy RU Ведение журнала синхронизации нескольких плееров @@ -20621,7 +20691,7 @@ DEBUG_SCAN FI Kaikkien hakujen kirjaus FR Journalisation de toutes les activités d'analyse IT Registrazione di tutte le analisi - NL Registratie van alle scanactiviteiten + NL Loggen van alle scanactiviteiten NO Loggføring av alle søk PL Rejestrowanie całego przeszukiwania RU Ведение журнала всех событий сканирования @@ -20651,7 +20721,7 @@ DEBUG_SCAN_IMPORT FI Tiedostojen ja soittoluetteloiden metatietojen tuonnin kirjaus FR Journalisation de l'importation des métadonnées des fichiers et des listes de lecture IT Registrazione importazione metadati da file e playlist - NL Importregistratie van metagegevens van bestand en playlist + NL Loggen import van metagegevens van bestand en playlist NO Loggføring av importering av metadata for filer og spillelister PL Rejestrowanie metadanych importowania plików i list odtwarzania RU Ведение журнала импорта метаданных файлов и плей-листов @@ -20681,7 +20751,7 @@ DEBUG_WIZARD FI Ohjatun asennuksen toimintojen kirjaus FR Journalisation des activités de l'assistant de configuration IT Registrazione attività procedura guidata di installazione - NL Registratie van installatiewizardactiviteit + NL Loggen van installatiewizardactiviteit NO Loggføring av aktivitet i konfigurasjonsveiviser PL Rejestrowanie działań Kreatora konfiguracji RU Ведение журнала действий мастера установки @@ -20696,7 +20766,7 @@ SETUP_DEBUG_SERVER_LOG FI Logitech Media Serverin lokitiedosto FR Fichier journal du Logitech Media Server IT File di registro di Logitech Media Server - NL Logboekbestand van Logitech Media Server + NL Logboek van Logitech Media Server NO Logitech Media Server loggfil PL Plik dziennika programu Logitech Media Server RU Файл журнала Logitech Media Server @@ -20711,7 +20781,7 @@ SETUP_DEBUG_SERVER_LOG_DESC FI Logitech Media Server pitää lokitiedostoa kaikista sovelluksiin liittyvistä toiminnoista (äänen virtautus, infrapuna jne.) täällä: FR Le Logitech Media Server conserve un fichier journal de toutes les activités liées à l'application (diffusion audio, infrarouge, etc.), à l'emplacement suivant: IT Logitech Media Server mantiene un file di registro per tutte le attività relative alle applicazioni (stream audio, raggi infrarossi e così via) nel seguente percorso: - NL Logitech Media Server bewaart hier een logboekbestand voor alle toepassingsactiviteiten (audio-streaming, infrarood, enz.): + NL Logitech Media Server bewaart hier een logboek voor alle toepassingsactiviteiten (audio-streaming, infrarood, enz.): NO Logitech Media Server fører en loggfil for alle programrelaterte aktiviteter (lydstrømming, infrarøde signaler osv.) her: PL Program Logitech Media Server przechowuje plik dziennika zawierający wszystkie działania dotyczące aplikacji (strumieniowe przesyłanie dźwięku, korzystanie z podczerwieni) w tym miejscu: RU Все действия приложений (потоковое аудио, ИК-выход и др.) записываются в файл журнала Logitech Media Server: @@ -20726,7 +20796,7 @@ SETUP_DEBUG_SCANNER_LOG FI Skannerin lokitiedosto FR Fichier journal de l'analyseur IT File di registro analisi - NL Logboekbestand van scanner + NL Logboek van scanner NO Loggfil for søk PL Plik dziennika modułu przeszukiwania RU Файл журнала сканера @@ -20741,7 +20811,7 @@ SETUP_DEBUG_SCANNER_LOG_DESC FI Logitech Media Server pitää lokitiedostoa kaikista hakuihin liittyvistä toiminnoista (mukaan lukien iTunes ja MusicIP) täällä: FR Le Logitech Media Server conserve un fichier journal de toutes les activités d'analyse, y compris pour iTunes et MusicIP, à l'emplacement suivant: IT Logitech Media Server mantiene un file di registro per tutte le attività relative all'analisi, inclusi iTunes e MusicIP, nel seguente percorso: - NL Logitech Media Server bewaart hier een logboekbestand voor alle scanactiviteiten (zoals iTunes en MusicIP): + NL Logitech Media Server bewaart hier een logboek voor alle scanactiviteiten (zoals iTunes en MusicIP): NO Logitech Media Server fører en loggfil for alle søkeaktiviteter, inkludert iTunes og MusicIP, her: PL Program Logitech Media Server przechowuje plik dziennika dla wszystkich działań związanych z przeszukiwaniem, w tym programem iTunes i MusicIP tutaj: RU В Logitech Media Server сохраняет файл журнала для всех действий сканирования, включая iTunes MusicIP, здесь: @@ -20786,7 +20856,7 @@ SETUP_DEBUG_PERFMON_LOG_DESC FI Logitech Media Server pitää lokitiedostoa suorituskyvystä täällä: FR Le Logitech Media Server conserve un fichier journal pour le contrôle des performances, à l'emplacement suivant: IT Logitech Media Server mantiene un file di registro per monitorare le prestazioni in questa posizione: - NL Logitech Media Server bewaart hier een logboekbestand voor prestatiecontrole: + NL Logitech Media Server bewaart hier een logboek voor prestatiecontrole: NO Logitech Media Server fører en loggfil for ytelsesovervåking her: PL Program Logitech Media Server przechowuje plik dziennika do monitorowania wydajności w tym miejscu: RU Файл журнала мониторинга производительности Logitech Media Server хранится здесь: @@ -21008,7 +21078,7 @@ DIRECTORY_PROGRESS FI Hakemistohaku FR Analyse du répertoire IT Analisi delle directory - NL Directory scannen + NL Mappen scannen NO Katalogsøk PL Przeszukiwanie katalogu RU Сканирование каталога @@ -21198,7 +21268,7 @@ UPDATESTANDALONEARTWORK_PROGRESS DE Nach aktualisierten Plattenhüllen suchen EN Find updated coverart files FR Recherche des pochettes des albums actualisés - NL Vind geüpdatete hoesafbeeldingen + NL Vind geüpdatete albumcovers NO Finn oppdaterte plateomslagsfiler PL Wyszukaj zaktualizowane pliki okładek @@ -21211,7 +21281,7 @@ PRECACHEARTWORK_PROGRESS FI Levykansien tallennus valmiiksi välimuistiin FR Mise en pré-cache de la pochette IT Pre-cache copertina - NL Hoesafbeeldingen vooraf in cache opslaan + NL Albumcovers vooraf in cache opslaan NO Hurtigbufring av omslag på forhånd PL Wstępnie buforowanie okładki RU Предварительное кэширование обложки @@ -21226,7 +21296,7 @@ DOWNLOADARTWORK_PROGRESS FI Kansitaidetta ladataan FR Téléchargement de la pochette IT Download copertina in corso - NL Hoesafbeeldingen downloaden + NL Albumcovers downloaden NO Laster ned omslag PL Pobieranie okładek RU Загрузка обложки @@ -21492,7 +21562,7 @@ PLUGINS_CHANGED_NEED_RESTART FI Muutokset tulevat voimaan, kun sovellus käynnistetään seuraavan kerran uudestaan. Käynnistä palvelin uudelleen nyt napsauttamalla tästä. FR Les modifications seront appliquées au prochain démarrage de l'application.Cliquez ici pour redémarrer le serveur maintenant. IT Le modifiche diventeranno effettive al successivo avvio dell'applicazione. Fare clic qui per riavviare il server ora. - NL Bij de volgende herstart van de toepassing worden wijzigingen voor deze plug-ins doorgevoerd. Klik hier om de server nu opnieuw te starten. + NL Bij de volgende herstart worden wijzigingen voor deze plug-ins doorgevoerd. Klik hier om de server nu opnieuw te starten. NO Endringer trer i kraft neste gang programmet startes på nytt. Klikk her for å starte serveren på nytt nå. PL Zmiany zostaną wprowadzone po następnym uruchomieniu aplikacji. Kliknij tutaj, aby uruchomić ponownie serwer teraz. RU Изменения будут применены при следующем перезапуске приложения. Щелкните здесь, чтобы перезагрузить сервер сейчас. @@ -21507,7 +21577,7 @@ RESTART_NOW FI Käynnistä uudelleen FR Redémarrer maintenant IT Riavvia ora - NL Nu opnieuw starten + NL Nu herstarten NO Start på nytt nå PL Uruchom ponownie teraz RU Перезапустить сейчас @@ -21522,7 +21592,7 @@ RESTART_LATER FI Käynnistä uudelleen myöhemmin FR Redémarrer ultérieurement IT Riavviare in un secondo momento - NL Later opnieuw opstarten + NL Later herstarten NO Start på nytt senere PL Uruchom ponownie później RU Перезапустить позже @@ -21537,7 +21607,7 @@ RESTARTING_PLEASE_WAIT FI Logitech Media Server käynnistetään uudelleen. Odota hetki ennen jatkamista... FR Le Logitech Media Server est en cours de redémarrage. Patientez une minute avant de continuer... IT È in corso il riavvio di Logitech Media Server. Attendere un momento prima di continuare... - NL Logitech Media Server wordt opnieuw gestart. Wacht even voordat je verdergaat... + NL Logitech Media Server wordt herstart. Wacht even voordat je verdergaat... NO Logitech Media Server startes på nytt. Vent litt før du fortsetter ... PL Trwa ponowne uruchamianie programu Logitech Media Server. Poczekaj chwilę, aby kontynuować... RU Logitech Media Server перезапускается. Подождите... @@ -21597,7 +21667,7 @@ PERSIST_DEBUG_SETTINGS FI Tallenna kirjausasetukset käytettäväksi, kun sovellus käynnistetään seuraavaksi uudelleen FR Enregistrer les réglages de journalisation pour une utilisation au prochain démarrage de l'application IT Salva e utilizza le impostazioni di registrazione al successivo avvio dell'applicazione - NL Registratie-instellingen opslaan voor gebruik bij volgende herstart van toepassing + NL Log-instellingen opslaan voor gebruik bij volgende herstart van toepassing NO Lagre innstillinger for loggføring og bruk dem ved neste programstart PL Zapisz ustawienia rejestrowania w celu użycia przy kolejnym uruchomieniu aplikacji RU Сохранить настройки журнала при перезапуске приложения @@ -21868,7 +21938,7 @@ JIVE_CHANGEPLAYERNAME_HELP FI Voit vaihtaa kirjainta vierityskiekolla ja valita kirjaimen keskipainikkeella. Vaihda soittimen nimi painamalla keskipainiketta uudelleen. FR Utilisez la molette pour changer les lettres, puis appuyez sur le bouton central pour effectuer la sélection. Appuyez à nouveau sur le bouton central pour modifier le nom de la platine. IT Utilizzare la rotellina per cambiare le lettere, quindi premere il pulsante centrale per selezionarle. Premere nuovamente il pulsante centrale per cambiare il nome del lettore. - NL Gebruik het scrollwiel om naar een andere letter te gaan. Druk dan op de middelste knop om die letter te selecteren. Druk nogmaals op de middelste knop om de naam van het muzieksysteem te wijzigen. + NL Gebruik het scrollwiel om naar een andere letter te gaan. Druk dan op de middelste knop om die letter te selecteren. Druk nogmaals op de middelste knop om de naam van de muziekspeler te wijzigen. NO Du kan endre bokstav med rullehjulet, og velge bokstav med midtknappen. Trykk på midtknappen igjen for å endre navnet på spilleren. PL Użyj pokrętła, aby zmienić litery, a następnie naciśnij środkowy przycisk w celu wybrania litery. Naciśnij ponownie środkowy przycisk, aby zmienić nazwę odtwarzacza. RU Прокручивайте колесико до нужной буквы и нажимайте для выбора центральную кнопку. Затем нажмите центральную кнопку еще раз, чтобы изменить имя плеера. @@ -21973,7 +22043,7 @@ JIVE_POPUP_ADDING FI Lisääminen FR Ajout IT Aggiunta - NL Bezig met toevoegen + NL Toevoegen NO Legger til PL Dodawanie RU Добавление @@ -21988,7 +22058,7 @@ JIVE_POPUP_REMOVING FI Poistetaan FR Suppression IT Rimozione - NL Bezig met verwijderen + NL Verwijderen NO Fjerner PL Usuwanie RU Удаление @@ -22358,7 +22428,7 @@ SAVED_SEARCH DE Gespeicherte Suche EN Saved Search FR Recherches enregistrées - NL Opgeslagen zoekresultaat + NL Opgeslagen zoekopdracht NO Lagret søk ALBUMS_SORT_METHOD @@ -22445,7 +22515,7 @@ PLAYER_BRIGHTNESS FI Soittimen kirkkaus FR Luminosité de la platine IT Luminosità lettore - NL Helderheid van muzieksysteem + NL Helderheid van muziekspeler NO Lysstyrke på spiller PL Jasność odtwarzacza RU Яркость плеера @@ -22550,7 +22620,7 @@ SETUP_MAXPLAYLISTLENGTH_DESC FI Jotta palvelimen muistia ei kuormiteta liikaa, soittoluettelon enimmäispituutta voidaan rajoittaa, mukaan luettuna Nyt soi -soittoluettelo. Jos arvo on 0, pituutta ei ole rajoitettu. Jos arvo annetaan, pienin sallittu on 10. FR La taille maximale de la liste de lecture, y compris de la liste de lecture en cours, peut être limitée afin de prévenir toute utilisation excessive de la mémoire système. La valeur 0 indique qu'aucune restriction n'est appliquée. La valeur minimale est de 10. IT Per evitare un uso eccessivo di memoria del server, è possibile limitare la lunghezza massima delle playlist, incluse quelle in Riproduzione in corso. Con il valore 0 non si impone alcun limite. Se si imposta un limite, il valore minimo è 10. - NL Teneinde de server tegen buitensporig geheugengebruik te beschermen, kan de maximumlengte van een playlist beperkt worden, inclusief de playlist Speelt nu. De waarde 0 betekent dat er geen beperking geldt. Indien ingesteld, is de minimumwaarde 10. + NL Om de server tegen hoog geheugengebruik te beschermen, kan de maximumlengte van een playlist worden beperkt, inclusief de playlist Speelt nu. De waarde 0 betekent dat er geen beperking geldt. Indien ingesteld, is de minimumwaarde 10. NO Makslengden for spillelister kan begrenses, inkludert Spilles nå-listen, for å beskytte serveren mot overdreven minnebruk. Verdien 0 angir ingen begrensning. Hvis en begrensning angis, er minimumsverdien 10. PL Aby ochronić serwer przed maksymalnym wykorzystaniem pamięci, maksymalna długość listy odtwarzania, włącznie z listą Teraz odtwarzane, może zostać ograniczona. Wartość 0 oznacza brak ograniczeń. Minimalna wartość w przypadku ustawienia wynosi 10. RU Чтобы защитить сервер от избыточного использования памяти, можно ограничить максимальную длину плей-листа, включая плей-лист "Исполняется". Минимальное значение ограничения равно 10. Значение 0 задает отказ от ограничения. @@ -22565,7 +22635,7 @@ ERROR_PLAYLIST_FULL FI Soittoluettelo on täynnä; raitoja ei lisätty FR La liste de lecture est pleine ; aucune piste n'a été ajoutée IT Playlist piena. Nessun brano aggiunto. - NL Playlist vol; geen tracks toegevoegd + NL Playlist vol; geen nummers toegevoegd NO Spillelisten er full, ingen spor ble lagt til PL Lista odtwarzania jest pełna; nie dodano żadnych ścieżek RU Плей-лист заполнен; дорожки не добавлены @@ -22580,7 +22650,7 @@ ERROR_PLAYLIST_ALMOST_FULL FI Soittoluettelo on täynnä: lisättiin %s/%s raitaa FR Liste de lecture pleine: %s pistes sur %s ont été ajoutées IT Playlist piena: aggiunti %s di %s brani - NL Playlist vol: %s van %s tracks toegevoegd + NL Playlist vol: %s van %s nummers toegevoegd NO Spillelisten er full: %s av %s spor lagt til PL Lista odtwarzania jest pełna: dodano ścieżek: %s z %s RU Плей-лист заполнен: добавлено %s из %s дорожек @@ -22883,7 +22953,7 @@ CONTROLPANEL_LOGFILES FI Lokitiedostot FR Fichiers journaux IT File di registro - NL Logboekbestanden + NL Logboeken NO Loggfiler PL Pliki dziennika RU Файлы журнала @@ -22928,7 +22998,7 @@ CONTROLPANEL_UPDATE_AVAILABLE FI Logitech Media Serverin päivitetty versio on käytettävissä ja valmiina asennusta varten. FR Une nouvelle version du Logitech Media Server est disponible et prête à l'installation. IT È disponibile una versione aggiornata di Logitech Media Server, pronta per essere installata. - NL Er is een bijgewerkte versie van Logitech Media Server beschikbaar voor installatie. + NL Bijgewerkte versie van Logitech Media Server beschikbaar voor installatie. NO En oppdatert versjon av Logitech Media Server er tilgjengelig og klar til installering. PL Zaktualizowana wersja programu Logitech Media Server jest dostępna i gotowa do zainstalowania. RU Доступна и готова к установке обновленная версия Logitech Media Server. @@ -22943,7 +23013,7 @@ PREFPANE_UPDATE_AVAILABLE FI Logitech Media Serverin päivitetty versio on käytettävissä ja valmiina asennusta varten. Käynnistä päivitys Logitech Media Server -kohdassa järjestelmäasetuksissa. FR Une version mise à jour du Logitech Media Server est disponible et peut être installée. Accédez à Logitech Media Server dans les Préférences système pour démarrer la mise à niveau. IT È disponibile una versione aggiornata di Logitech Media Server, pronta per essere installata. Per avviare l'aggiornamento, accedere a Logitech Media Server in Preferenze di Sistema. - NL Er is een bijgewerkte versie van Logitech Media Server beschikbaar voor installatie. Ga naar Logitech Media Server in Systeemvoorkeuren om de upgrade te starten. + NL Bijgewerkte versie van Logitech Media Server beschikbaar voor installatie. Ga naar Logitech Media Server in Systeemvoorkeuren om de upgrade te starten. NO En oppdatert versjon av Logitech Media Server er tilgjengelig og klar til installering. Du kan starte oppgraderingen fra Logitech Media Server i Systemvalg. PL Zaktualizowana wersja programu Logitech Media Server jest gotowa i dostępna do zainstalowania. Aby rozpocząć uaktualnianie, otwórz ekran programu Logitech Media Server w programie Preferencje systemowe. RU Доступна и готова к установке обновленная версия сервера Logitech Media Server. Чтобы начать установку обновления, посетите Logitech Media Server в приложении System Preferences. @@ -22958,7 +23028,7 @@ CONTROLPANEL_NO_UPDATE_AVAILABLE FI Logitech Media Serveristä ei ole saatavilla päivitettyä versiota. FR Aucune nouvelle version du Logitech Media Server n'est disponible. IT Non è disponibile alcuna versione aggiornata di Logitech Media Server. - NL Er is geen bijgewerkte versie van Logitech Media Server beschikbaar. + NL Geen bijgewerkte versie van Logitech Media Server beschikbaar. NO Det finnes ingen oppdatert versjon av Logitech Media Server tilgjengelig. PL Brak dostępnej zaktualizowanej wersji programu Logitech Media Server. RU Нет доступных обновленных версий Logitech Media Server. @@ -23033,7 +23103,7 @@ CONTROLPANEL_DIAGNOSTICS FI Diagnostiikka FR Diagnostic IT Diagnostica - NL Diagnostische gegevens + NL Diagnose NO Diagnose PL Diagnostyka RU Диагностика @@ -23138,7 +23208,7 @@ CONTROLPANEL_SN_FAILURE_DESC FI Tämä saattaa olla tilapäinen internet-ongelma tai internet-palveluntarjoajasi määrittämä rajoitus. Jos ongelma jatkuu, tarkista, ettei palomuurisi estä lähtevää liikennettä. FR Il s'agit d'un problème Internet temporaire ou d'une limitation imposée par votre FAI. Si le problème persiste, vérifiez que votre pare-feu ne bloque pas le trafic sortant. IT Potrebbe trattarsi di un problema Internet temporaneo o di una limitazione imposta dal provider di servizi Internet. Se il problema persiste, verificare che il traffico in uscita non sia bloccato dal firewall. - NL Dit is misschien een tijdelijk internetprobleem, of een beperking die door je internetprovider is opgelegd. Als het probleem zich blijft voordoen, zorg er dan voor dat je firewall geen uitgaand verkeer blokkeert. + NL Dit is misschien een tijdelijk internetprobleem, of een beperking die door je internetprovider is opgelegd. Als het probleem zich blijft voordoen, zorg dan dat je firewall geen uitgaand verkeer blokkeert. NO Dette kan skyldes et midlertidig problem med Internett, eller en begrensning fra Internett-leverandøren. Sjekk at brannmuren ikke blokkerer utgående trafikk hvis problemet vedvarer. PL Może to być tymczasowy problem z Internetem lub ograniczenie narzucone przez usługodawcę internetowego. Jeżeli problem nie zniknie, upewnij się, że zapora nie blokuje ruchu wychodzącego. RU Возможно, это временная проблема с Интернетом или ограничение поставщика услуг Интернета. Если проблема не исчезнет, убедитесь, что брандмауэр не блокирует исходящий трафик. @@ -23153,7 +23223,7 @@ CONTROLPANEL_SN_PROXY FI Logitech Media Server on määritetty käyttämään välityspalvelinta. Tässä kokoonpanossa yhteyttä sivustoon mysqueezebox.com portin 3483 kautta ei voi testata. FR La configuration de votre Logitech Media Server indique l'utilisation d'un serveur proxy. Cette configuration ne permet pas de tester la connectivité à mysqueezebox.com sur le port 3483. IT È stato rilevato che Logitech Media Server è configurato per utilizzare un server proxy. Con questa configurazione non è possibile verificare la connessione con mysqueezebox.com sulla porta 3483. - NL Je Logitech Media Server is geconfigureerd voor gebruik van een proxyserver. In deze configuratie kan de verbinding met 'mysqueezebox.com' op poort 3483 niet getest worden. + NL Je Logitech Media Server is geconfigureerd voor gebruik van een proxyserver. In deze configuratie kan de verbinding met 'mysqueezebox.com' op poort 3483 niet worden getest. NO Logitech Media Server er konfigurert til å bruke en proxy-server. Med denne konfigureringen kan ikke tilkoplingen til mysqueezebox.com på port 3483 testes. PL Program Logitech Media Server jest skonfigurowany w celu użycia serwera proxy. W przypadku takiej konfiguracji nie można przetestować połączenia z witryną mysqueezebox.com na porcie 3483. RU Обнаружено, что ваш Logitech Media Server настроен на использование прокси-сервера. В такой конфигурации подключение к сайту mysqueezebox.com на порту 3483 невозможно проверить. @@ -23213,7 +23283,7 @@ CONTROLPANEL_CONFLICT_CISCOVPNSTATEFULINSPECTION FI Jos Logitech Media Serverin kanssa on yhteysongelmia, varmista, että Tilallinen palomuuri (aina käytössä) -valinta ei ole valittuna. FR Si vous rencontrez des problèmes de connectivité avec le Logitech Media Server, vérifiez que l'option "Pare-feu stateful (toujours activé)" est désactivée. IT In caso di problemi di connessione con Logitech Media Server, verificare che il firewall con stato sia disattivato. - NL Ondervind je verbindingsproblemen met Logitech Media Server, zorg er dan voor dat 'Stateful Firewall (Altijd aan)' is uitgeschakeld + NL Ondervind je verbindingsproblemen met Logitech Media Server, zorg dan dat 'Stateful Firewall (Altijd aan)' is uitgeschakeld NO Hvis du har tilkoplingsproblemer med Logitech Media Server, må du sørge for at "Tilstandsbevisst brannmur (Alltid på)" er slått av PL W przypadku wystąpienia problemów z łącznością w programie Squeezebox, upewnij się, że opcja „Zapora stanowa (zawsze włączona)” jest wyłączona RU При возникновении проблем с подключением к Logitech Media Server убедитесь, что параметр "Брандмауэр с отслеживанием состояния (Всегда включено)" отключен @@ -23424,7 +23494,7 @@ CLEANUP_FILECACHE FI Poista kansitaiteen, mallien ym. tiedostovälimuisti FR Supprimer le cache des pochettes, modèles, etc. IT Elimina cache per copertine, modelli e così via - NL Bestandscache voor hoesafbeeldingen, sjablonen, enz. verwijderen + NL Bestandscache voor albumcovers, sjablonen, enz. verwijderen NO Slett filhurtigbuffer for omslag, maler osv. PL Usuń bufor plików okładek, szablonów itp. RU Удалить файловый кэш для обложек, шаблонов и др. @@ -23454,7 +23524,7 @@ CLEANUP_LOGS FI Poista lokitiedostot FR Supprimer les fichiers journaux IT Elimina file di registro - NL Logboekbestanden verwijderen + NL Logboeken verwijderen NO Slett loggfiler PL Usuń pliki dziennika RU Удалить файлы журнала @@ -23469,7 +23539,7 @@ CLEANUP_CACHE FI Tyhjennä välimuistikansio, mukaan luettuna mediatietokanta ja kansitaiteen välimuisti. FR Nettoyer le dossier cache, y compris la base de données de la bibliothèque multimédia, les pochettes, etc. IT Pulisci cartella della cache, inclusi il database della libreria multimediale, la cache delle copertine e così via. - NL Cachemap opschonen, inclusief mediadatabase, hoesafbeeldingencache, enz. + NL Cachemap opschonen, inclusief mediadatabase, albumcovercache, enz. NO Tøm hurtigbuffermappen, inkludert mediedatabasen, hurtigbufrede omslag osv. PL Czyści folder pamięci podręcznej, w tym bazę danych biblioteki multimedialnej, pamięć podręczną okładek itp. RU Очистить папку кэша, включая базу данных библиотеки мультимедиа, кэш обложек и др. @@ -23514,7 +23584,7 @@ CLEANUP_DELETING FI Poistetaan FR Suppression en cours IT Eliminazione in corso - NL wordt verwijderd + NL Verwijderen NO Sletter PL Usuwanie RU Удаление @@ -23529,7 +23599,7 @@ CONTROLPANEL_NO_STATUS FI Tilatietoja ei ole saatavilla. Huomaa, että Logitech Media Serverin on oltava käynnissä, jotta sen tilatiedot voitaisiin näyttää. FR Aucune information sur l'état n'est disponible. Notez que le Logitech Media Server doit être en cours d'utilisation pour afficher ces informations. IT Non sono disponibili informazioni sullo stato. Per visualizzare le informazioni sullo stato è necessario che Logitech Media Server sia in esecuzione. - NL Er is geen statusinformatie beschikbaar. Logitech Media Server moet actief zijn om statusinformatie te kunnen weergeven. + NL Geen statusinformatie beschikbaar. Logitech Media Server moet actief zijn om statusinformatie te kunnen weergeven. NO Ingen statusinformasjon er tilgjengelig. Logitech Media Server må kjøre for at den skal kunne vise statusinformasjon. PL Brak dostępnych informacji o stanie. Aby możliwe było wyświetlenie informacji o stanie programu Logitech Media Server, musi on być uruchomiony. RU Сведения о состоянии недоступны. Сведения о состоянии выводятся только в том случае, если Logitech Media Server выполняется. @@ -23589,7 +23659,7 @@ RUN_FAILSAFE FI Suorita ilman käyttäjätunnisteita (vikasietotila) FR Exécuter sans poste utilisateur ("Mode sans échec") IT Esegui senza estensioni utente (modalità provvisoria) - NL Zonder gebruikersextensies uitvoeren ('Veilige modus') + NL Zonder gebruikersextensies uitvoeren ('Veilige modus') NO Kjør uten brukerutvidelser ("sikkermodus") PL Uruchom bez rozszerzeń użytkownika („tryb bezpieczny”) RU Запустить без пользовательских расширений ("Безопасный режим") @@ -23859,7 +23929,7 @@ HOWTO_PAUSE FI Voit keskeyttää raidan toiston napauttamalla taukopainiketta. Voit lopettaa raidan toiston kokonaan pitämällä taukopainikkeen pohjassa. FR Appuyez sur Pause pour mettre la lecture en pause. Pour arrêter la lecture, maintenez le bouton Pause enfoncé. IT Per sospendere la riproduzione del brano, premere leggermente il pulsante di PAUSA. Per interrompere il brano, tenere premuto tale pulsante. - NL Tik op PAUSE om het nummer te pauzeren. Houd de knop PAUSE ingedrukt om het nummer te stoppen. + NL Druk op PAUSE om het nummer te pauzeren. Houd de knop PAUSE ingedrukt om het nummer te stoppen. NO Hvis du vil sette sporet på pause, trykker du lett på PAUSE. Hvis du vil stoppe sporet, holder du nede PAUSE-knappen. PL Aby wstrzymać odtwarzanie, wybierz przycisk PAUSE. Aby zatrzymać odtwarzanie, naciśnij i przytrzymaj przycisk PAUSE. RU Чтобы приостановить дорожку, коснитесь кнопки PAUSE. Чтобы остановить дорожку, нажмите и удерживайте кнопку PAUSE. @@ -23904,7 +23974,7 @@ REDUCED_TO_PREVENT_CLIPPING FI %s leikkauksen estämiseksi FR %s pour empêcher l'ecrêtage IT %s per impedire il clipping - NL %s om knipbewerkingen te voorkomen + NL %s om clipping te voorkomen NO %s for å forhindre klipping PL %s, aby zapobiec obcinaniu RU %s для предотвращения амплитудного ограничения @@ -24026,7 +24096,7 @@ PLUGINS_UPDATES_AVAILABLE FI Laajennuspäivitykset ovat saatavissa FR Des mises à jour des plugins sont disponibles IT Sono disponibili aggiornamenti per il plugin - NL Er zijn plug-inupdates beschikbaar + NL Plug-inupdates beschikbaar NO Oppdateringer til plugin-moduler er tilgjengelige PL Dostępne są nowe aktualizacje. RU Доступны обновления подключаемых модулей @@ -24041,7 +24111,7 @@ PLUGINS_RESTART_MSG FI Laajennukset on päivitetty - ohjelma on käynnistettävä uudelleen FR Les plugins ont été mis à jour. Vous devez redémarrer le serveur. IT Sono stati aggiornati dei plugin. È necessario riavviare. - NL Plug-ins zijn bijgewerkt - Opnieuw opstarten vereist + NL Plug-ins zijn bijgewerkt - Herstart vereist NO Plugin-modulene er oppdatert. Du må starte programmet på nytt. PL Dodatki zostały zaktualizowane – wymagane jest ponowne uruchomienie. RU Подключаемые модули были обновлены. Требуется перезагрузка. @@ -24056,7 +24126,7 @@ NO_LIBRARY FI Mediakirjastoa ei ole määritetty FR Aucune bibliothèque multimédia n'a été configurée IT Nessuna libreria multimediale configurata - NL Er is geen mediacollectie geconfigureerd + NL Geen mediacollectie geconfigureerd NO Ingen mediebiblioteker er konfigurert PL Nie skonfigurowano biblioteki multimedialnej RU Не настроено ни одной библиотеки мультимедиа @@ -24213,7 +24283,7 @@ SYNC_ABOUT FI Lisää yksi tai useampi Squeezebox, jotta voit käyttää synkronointitoimintoa ja toteuttaa monihuoneäänijärjestelmän mahdollisuudet. Lisätietoja on osoitteessa Logitech.com. FR Ajoutez une ou plusieurs Squeezebox à synchroniser et explorez tout le potentiel de votre système audio multipièce. Visitez le site Web Logitech.com pour obtenir plus d'informations. IT Aggiungere uno o più Squeezebox per utilizzare la funzione Sincronizza, ottenendo il massimo dal sistema audio per più locali. Per ulteriori informazioni, visitare il sito Web Logitech.com. - NL Voeg een of meer Squeezebox-muzieksystemen toe om ze te synchroniseren voor optimale audio in meerdere kamers tegelijk. Ga naar Logitech.com voor meer informatie. + NL Voeg een of meer Squeezebox-muziekspelers toe om ze te synchroniseren voor optimale audio in meerdere kamers tegelijk. Ga naar Logitech.com voor meer informatie. NO Legg til én eller flere Squeezebox-spillere dersom du ønsker å bruke synkroniseringsfunksjonen til å få mest mulig ut av flerromslyd. Besøk Logitech.com for mer informasjon. PL Dodaj jeden lub więcej urządzeń Squeezebox, aby użyć funkcji synchronizacji i wykorzystać w pełni możliwości odtwarzania dźwięku w wielu pomieszczeniach. Więcej informacji można znaleźć w witrynie Logitech.com. RU Добавьте еще один или несколько плееров Squeezebox, чтобы использовать функцию синхронизации и реализовать полный потенциал многокомнатной аудиосистемы. Дополнительные сведения см. на сайте Logitech.com. @@ -24300,7 +24370,7 @@ SERVER_UPDATE_AVAILABLE FI Logitech Media Serveristä on tarjolla uusi versio (%s). Lataa se napsauttamalla tässä. FR Une nouvelle version de Logitech Media Server est disponible (%s). Cliquez ici pour la télécharger. IT È disponibile una nuova versione di Logitech Media Server (%s). Fare clic qui per scaricarla. - NL Er is een nieuwe versie van Logitech Media Server beschikbaar (%s). Klik op hier om deze te downloaden. + NL Een nieuwe versie van Logitech Media Server is beschikbaar (%s). Klik op hier om deze te downloaden. NO En ny versjon av Logitech Media Server er tilgjengelig (%s). Klikk her for å laste den ned. PL Dostępna jest nowa wersja oprogramowania Logitech Media Server (%s). Kliknij tutaj, aby pobrać. RU Доступна новая версия Logitech Media Server (%s). Для загрузки щелкните здесь. @@ -24315,7 +24385,7 @@ SERVER_UPDATE_AVAILABLE_SHORT FI Logitech Media Serveristä on tarjolla uusi versio. FR Une nouvelle version de Logitech Media Server est disponible. IT È disponibile una nuova versione di Logitech Media Server. - NL Er is een nieuwe versie van Logitech Media Server beschikbaar. + NL Een nieuwe versie van Logitech Media Server is beschikbaar. NO En ny versjon av Logitech Media Server er tilgjengelig. PL Dostępna jest nowa wersja oprogramowania Logitech Media Server. RU Доступна новая версия Logitech Media Server. @@ -24325,7 +24395,7 @@ SERVER_LINUX_UPDATE_AVAILABLE DE Eine neue Version von Logitech Media Server ist verfügbar (%s). Klicken Sie hier für weitere Instruktionen. EN A new version of Logitech Media Server is available (%s). Click here for further information. FR Une nouvelle version de Logitech Media Server est disponible (%s). Cliquez ici pour plus d'info. - NL Er is een nieuwe versie van Logitech Media Server beschikbaar (%s). Klik op hier. + NL Een nieuwe versie van Logitech Media Server is beschikbaar (%s). Klik op hier. NO En ny versjon av Logitech Media Server er tilgjengelig (%s). Klikk her for å få mer informasjon. PL Nowa wersja Logitech Media Server jest dostępna (%s). Kliknij tutaj w celu uzyskania dodatkowej wiadomości. @@ -24333,3 +24403,22 @@ UPDATES_LATEST_CHANGES DE Letzte Änderungen (alle Änderungen) EN Latest Changes (all changes) NO Seneste endringer (all changes) + NL Laatste wijzigingen (all changes) + +SERVICE_REQUIRES_HTTPS + DE Dieser Dienst benötigt das Perl Modul IO::Socket::SSL! Ohne dieses Modul ist die Nutzung diese Dienstes nicht möglich. Bitte benutzen Sie den Paket-Manager ihres Betriebssystems um es zu installieren. + EN This service requires the Perl module IO::Socket::SSL. You can not use this service without that module. Please use your operating system's package manager to install it. + NL Deze service vereist Perl module IO::Socket::SSL. Je kunt de service zonder deze module niet gebruiken. Gebruik de pakketmanager van je besturingssysteem om het te instaleren. + +UPDATE + CS Aktualizace + DA Opdatering + DE Aktualisierung + EN Update + ES actualización + FI Päivittää + FR Mise à jour + IT Aggiornamento + NL Update + NO Oppdaterer + PL Aktualizacja diff --git a/types.conf b/types.conf index f1458ed7af6..0e901fe368a 100644 --- a/types.conf +++ b/types.conf @@ -10,6 +10,7 @@ ######################################################################### aif aif,aiff audio/x-aiff audio alc - audio/x-m4a-lossless audio +alcx - - audio ape ape audio/monkeys-audio audio app app,class application/x-java-applet - asx asx,wax video/asx,application/asx,application/vnd.ms-asf,video/x-ms-asf,audio/x-ms-wax,audio/x-ms-asf,video/x-ms-wvx playlist @@ -35,9 +36,13 @@ lnk lnk application/windowsshortcut list m3u m3u,m3u8 audio/mpegurl,audio/x-mpegurl playlist aac aac audio/aac,audio/aacp audio mp4 m4a,mp4,m4b audio/m4a,audio/x-m4a,audio/mp4 audio +mp4x - - audio mp3 mp2,mp3 audio/mpeg,audio/mp3,audio/mp3s,audio/x-mpeg,audio/mpeg3,audio/mpg audio mpc mpc,mp+ audio/x-musepack audio ogg ogg,oga audio/x-ogg,application/ogg,audio/ogg,application/x-ogg audio +# Special content type for Ogg FLAC streams (which use a different decode path) +ogf - - audio +ops opus audio/opus,audio/ogg;codecs=opus audio pcm pcm audio/L16,audio/x-pcm audio pdf pdf application/pdf - pls pls audio/scpls,audio/x-scpls playlist @@ -47,7 +52,7 @@ gd gd image/gd - sls - audio/x-m4a-sls audio swf swf application/x-shockwave-flash - txt txt text/plain - -wav wav,wave audio/x-wav audio +wav wav,wave audio/x-wav,audio/wav,audio/vnd.wave,audio/wave audio wma wma audio/x-ms-wma,application/vnd.ms.wms-hdr.asfv1,application/x-mms-framed,audio/asf audio wmal - audio/x-wma-lossless audio wmap - audio/x-wma-wmapro audio