[Neues Modul] Spotify

Begonnen von neumann, 28 Mai 2017, 15:58:19

Vorheriges Thema - Nächstes Thema

Tomatenjoghurt

Perfekt, hab mich für die Variante von CoolTux entschieden, funktioniert super! :)

Falls es wen interessiert...ich setze mir via FTUI über eine select-box eine playlist, die dann im state von besagtem dummy gespeichert wird. Ein klick auf den play button mit dem befehl von CoolTux spielt dann die ausgewählte playlist ab.
Wirklich super, danke!  ;D

enrikb

[Spotify Connect]

Zitat von: neumann am 02 Juni 2017, 22:42:29
Falls jemand eine Idee hat und helfen mag, sehr gerne!

Also, ich habe mal ein bisschen weiter gespielt und die Informationen von spotcontrol und librespot in einen perl proof-of-concept übersetzt, um etwas Licht ins Dunkel zu bringen. Damit sich ein spotify-connect client mit einem Account verbindet, schickt man ihm offenbar einen 'addUser'-Request, der mehrfach verschlüsselte und verschleierte wiederverwendbare Logininformationen enthält. Wenn man die Spotify-Anwendung dazu bringt, ein nachgebautes connect device zu akzeptieren und das dann als Ziel auswählt, schenkt einem die Anwendung diese Logininformationen als 'blob'. Möglicherweise kann man in diesem 'blob' auch ein OAuth-Token verwenden. Was man von der Spotify-Anwendung bekommt, scheint aber ein volles, nicht ablaufendes Äquivalent von Nutzername/Passwort zu sein.

Hier ist ein kleiner in perl heruntergeschriebener Server, der die entsprechenden Anfragen von der Spotify-Anwendung verarbeitet und nach einem addUser die Daten auf STDERR ausgibt:


#!/usr/bin/perl -w

use Math::BigInt;
use Data::UUID;

$priv_key = Math::BigInt->from_hex(
  'cafebabe_cafebabe_cafebabe_cafebabe_cafebabe_cafebabe'.
  'cafebabe_cafebabe_cafebabe_cafebabe_cafebabe_cafebabe'.
  'cafebabe_cafebabe_cafebabe_cafebabe_cafebabe_cafebabe'.
  'cafebabe_cafebabe_cafebabe_cafebabe_cafebabe_cafebabe'
);

$local_id = Data::UUID->new->create_from_name_str(Data::UUID::NameSpace_URL, "cafebabe.fake.spotify.com");

{
  package MyConnect;

  use HTTP::Server::Simple::CGI;
  use base qw(HTTP::Server::Simple::CGI);

  use MIME::Base64;
  use JSON::PP;
  use Crypt::DH;
  use Crypt::Rijndael;
  use Crypt::PBKDF2;
  use Digest::SHA qw(sha1);
  use Digest::HMAC_SHA1 qw(hmac_sha1);

  my $dh = Crypt::DH->new;

  my %dispatch = (
    '/' => \&resp_conf,
    # ...
  );

  sub post_setup_hook {
    my $self = shift;
    # First Oakley Default Group
    $dh->p(Math::BigInt->from_hex(
        'FFFFFFFF_FFFFFFFF_C90FDAA2_2168C234_C4C6628B_80DC1CD1'.
        '29024E08_8A67CC740_20BBEA6_3B139B22_514A0879_8E3404DD'.
        'EF9519B3_CD3A431B_302B0A6D_F25F1437_4FE1356D_6D51C245'.
        'E485B576_625E7EC6_F44C42E9_A63A3620_FFFFFFFF_FFFFFFFF'
      ));
    $dh->g(2);
    $dh->priv_key($main::priv_key);
    $dh->generate_keys;
    $self->SUPER::post_setup_hook();
  }

  sub handle_request {
    my $self = shift;
    my $cgi  = shift;

    my $path = $cgi->path_info();
    my $handler = $dispatch{$path};

    if (ref($handler) eq "CODE") {
      $handler->($cgi);

    } else {
      print "HTTP/1.0 404 Not found\r\n";
      print $cgi->header,
      $cgi->start_html('Not found'),
      $cgi->h1('Not found'),
      $cgi->end_html;
    }
  }

  sub resp_conf {
    my $cgi  = shift;   # CGI.pm object
    return if !ref $cgi;

    my $action = $cgi->param('action');
    my $pub_key = "blurb";

    if ($action eq "getInfo") {
      print "HTTP/1.0 200 OK\r\n";
      print $cgi->header("application/json"),

      encode_json({
          status => 101,
          statusString => "OK",
          spotifyError => 0,
          version => "2.1.0",
          deviceID => $main::local_id,
          remoteName => "Da Fake Speaker",
          activeUser => "",
          publicKey => encode_base64(pack('H*', substr($dh->pub_key->as_hex, 2)), ''),
          deviceType => "SPEAKER",
          libraryVersion => "1.20.0-g594175d4",
          accountReq => "PREMIUM",
          brandDisplayName => "MyConnector",
          modelDisplayName => "HAL 9000",
        });
      print "\n";
    } elsif ($action eq "addUser") {

      decode_and_dump($cgi);

      print "HTTP/1.0 200 OK\r\n";
      print $cgi->header("application/json"),

      encode_json({
          status => "101",
          statusString => "ERROR-OK",
          spotifyError => 0,
        });
      print "\n";
    } else {
      print "HTTP/1.0 404 Not found\r\n";
      print $cgi->header,
      $cgi->start_html('Not found'),
      $cgi->h1('Not found'),
      $cgi->end_html;
    }
  }

  sub decode_and_dump {
    my $cgi = shift;

    my $local_id = $main::local_id;
    my $username = $cgi->param('userName');
    my $client_key = Math::BigInt->from_hex(unpack('H*', decode_base64($cgi->param('clientKey'))));
    my $encrypted_blob = decode_base64($cgi->param('blob'));

    my $shared_key = $dh->compute_secret($client_key);
    my $base_key       = substr(sha1(pack('H*', substr($shared_key->as_hex, 2))), 0, 16);
    my $checksum_key   = hmac_sha1("checksum",   $base_key);
    my $encryption_key = substr(hmac_sha1("encryption", $base_key),0 , 16);

    my $iv        = substr($encrypted_blob, 0, 16);
    my $encrypted = substr($encrypted_blob, 16, -20);
    my $cksum     = substr($encrypted_blob, -20);

    my $mac = hmac_sha1($encrypted, $checksum_key);

    #die "Bad checksum!" unless ($mac eq $cksum);
    return unless ($mac eq $cksum);

    # outer decryption
    my $aes = new Crypt::Rijndael($encryption_key, Crypt::Rijndael::MODE_CTR());
    $aes->set_iv($iv);
    my $decrypted = $aes->decrypt($encrypted);
    $decrypted = decode_base64($decrypted);

    # inner decryption
    my $pbkdf2 = Crypt::PBKDF2->new(
      hash_class => 'HMACSHA1',
      iterations => 0x100,
      output_len => 20,
      salt_len => length($username),
    );

    my $blob_key = $pbkdf2->PBKDF2($username, sha1($local_id));
    $blob_key  = sha1($blob_key);
    $blob_key .= pack('N', length($blob_key));

    $aes = new Crypt::Rijndael($blob_key, Crypt::Rijndael::MODE_ECB());
    $decrypted = $aes->decrypt($decrypted);

    # inner obfuscation
    my @decrypted = split('', $decrypted);
    my $l = length($decrypted);
    for (my $i = 0; $i < $l - 16; $i++) {
      $decrypted[$l - $i - 1] ^= $decrypted[$l - $i - 17];
    }

    print STDERR encode_json(
      {
        Username => $username,
        DecodedBlob => encode_base64(join('', @decrypted), ''),
      }
    ), "\n";

  }
}

MyConnect->new(8080, Socket::AF_INET6)->run();


Damit der gefunden wird, muss man ihn noch per Bonjour ankündigen. Der Einfachheit halber habe ich das mit avahi gemacht:


$ avahi-publish-service -v FakeSpeaker _spotify-connect._tcp 8080 VERSION=1.0 CPath=/
Server version: avahi 0.6.32-rc; Host name: chekov.local
Established under name 'FakeSpeaker'


Wenn man diese Daten erstmal hat, kann man sie umgekehrt für sein Wunschdevice wieder verschlüsseln und per addUser dorthin schicken. Das habe ich noch nicht implementiert oder gar getestet. Allerdings hat spotcontrol die Ausgabe des mini-Servers, abgespeichert als blob-Datei dafür, akzeptiert und erfolgreich verwendet.

LeoSum

#77
Heyho,
erstmal vielen Dank für das tolle Modul!
Bislang hatte ich spotifyd-http (https://github.com/Spotifyd/spotifyd-http) eingesetzt und curl commands von FHEM aus gesendet um librespot fernzusteuern.
Das ist nun mit deinem Modul viel einfacher und ich spare mir eine Softwarekomponente.

Allerdings habe ich ein Problem:

Ich möchte auf Knopfdruck die Wiedergabe fortsetzen wenn sie pausiert ist und gleichzeitig auf meinen librespot player übertragen, falls das nicht schon der aktive ist.
Dazu führe ich folgende Aktion durch ein Notify aus:

(set SpotifyWeb resume ; set SpotifyWeb transferPlayback Librespot)
Allerdings wird hier scheinbar zufällig stets nur einer der beiden Befehle ausgeführt.

Ist das vielleicht ein Timing-Problem durch meinen schwachbrüstigen Raspberry Pi 2? Könnte man im Modul mehrere Commands irgendwie queuen und verzögert rausschicken?

EDIT: Ich habe jetzt erstmal ein "sleep 1" zwischen die beiden Kommandos geklemmt, damit werden alle Kommandos sicher ausgeführt.


Einen Wunsch hätte ich noch @neumann:

librespot unterstützt aktuell kein shuffle, und das wird sich wohl mittelfristig auch nicht ändern.
Siehst du eine Möglichkeit, bei der Funktion "playRandomTrackFromPlaylistByURI" noch eine Zahl als weiteren Parameter anzuhängen um so beispielsweise 20 random tracks aus einer Playlist in die Warteschlange zu packen? So könnte man diesen Mangel in librespot umschiffen.

neumann

Hey LeoSum,

danke für das Feedback!
Der Einfachheit halber habe ich nun einfach das resume command um die Möglichkeit zur Angabe eines Zielgeräts erweitert, damit sollte dein Problem in jedem Fall behoben sein. Das Update ist im SVN und wird dann morgen via "update" zur Verfügung stehen.

Die Erweiterung für die random Playlist habe ich mir mal für die Zukunft aufgeschrieben :)

Liebe Grüße
Oskar
Modulentwickler
- Spotify #72490
- Nello #75127

neumann

Hey enrikb,

sehr cool, da hast du ja echt schon einiges geschafft.
Damit es praktikabel ist und in das Modul rein kann müssten geklärt sein wie man den Blob bekommt und die Wiedergabe startet.
Ich bin gespannt was du noch rausfindest, danke für deine Arbeit!

LG
Oskar
Modulentwickler
- Spotify #72490
- Nello #75127

AmunRe

Zitat von: LeoSum am 14 Juni 2017, 20:14:01


librespot unterstützt aktuell kein shuffle, und das wird sich wohl mittelfristig auch nicht ändern.
Siehst du eine Möglichkeit, bei der Funktion "playRandomTrackFromPlaylistByURI" noch eine Zahl als weiteren Parameter anzuhängen um so beispielsweise 20 random tracks aus einer Playlist in die Warteschlange zu packen? So könnte man diesen Mangel in librespot umschiffen.


Das wäre das, was ich mir auch wünsche, der trick play, shuffle und next track klappt nämlich nicht.
4 x Echo Dot, HMLAN Gateway, und diverse HM Komponenten, Philips Hue + OSRAM Plugs

LeoSum

@neumann: schön zu hören, danke!

@AmunRe: ich hatte zuvor, als ich noch mit spotifyd-httpd gesteuert habe, eine python script gehabt, welches alle Song URIs einer Playlist URI in zufälliger Reihenfolge ausgegeben hat. Diese SongURI-Liste konnte man dann per spotifyd-httpd auf librespot ausgeben lassen. Das war dann wie shuffle.

@neumann: wäre es möglich dein Modul eine beliebig lange liste an SongURIs annehmen zu lassen? Dann könnte ich mein Skript weiterverwenden.

neumann

LeoSum, ja das ist möglich, du kannst bei playTrackByURI mehrere URIs nacheinander angeben (und ganz am Ende das Zielgerät falls gewünscht).
Ich werde das Feature auch demnächst genauso wie du beschrieben hast implementieren, gebt mir noch ein bisschen Zeit.
Modulentwickler
- Spotify #72490
- Nello #75127

LeoSum

Klingt super! Ich freue mich drauf

jove01

Hallo

Leider habe ich dieses Modul nicht wirklich geschnallt. :-[

Ich habe mir gerade einen kostenlosen Account geholt, wohlwissend, dass damit viele Funktionen nicht funktionieren.

Die Definition des Spotify-Devices gelang relativ problemlos. Die Spotify-App habe ich auf einem Laptop und auf ein Tablet installiert.

Eine Url konnte ich kopieren . Aber mit den verschieden Playfunktionen im FHEM-Device startet nichts. STATE bleibt auf paused.

Auch mit transferPlayback komme ich nicht weiter, um eines der beiden Spotfy-Devices anzusprechen.

Wo ist mein Denkfehler, auch wenn alle anderen das schnallen ?

Auf eine Soundtouch 10 kann ich mit dem kostenlosen Spotfy nichts übertragen, obwohl es in der App gesehen wird.

Das ist aber sicher alles in den Griff zu kriegen, wenn ich hier den entscheidenden Tip bekomme. Aber mein eigentliches Ansinnen scheint wohl generell nicht zu funktionieren.:

Über den DLNA-Renderer spiele ich Radiostreams auf meinen Marantz-AV. Hier hoffte ich, Spotify irgendwie reinzubekommen. Seht Ihr da eine Chance ?

Danke
Jürgen

Aktuelles FHEM auf Raspi 3 und dbLog
CUL 433
HMLan Rolladensteuerung

neumann

Hey Jürgen,

leider funktionieren die meisten Funktionen tatsächlich nur mit einem Spotify Premium Account...
Fast die gesamte API ist darauf beschränkt.

Liebe Grüße
Oskar
Modulentwickler
- Spotify #72490
- Nello #75127

jove01

Hallo Oskar

Danke für deine Antwort.
Aber selbst mit dem kostenlosen Spotify müsste ich doch mit deinem Modul eine Wiedergabe auf eines meiner geräte mit Spotify starten können, oder verstehe ich alles falsch.
Aktuelles FHEM auf Raspi 3 und dbLog
CUL 433
HMLan Rolladensteuerung

neumann

Ich glaube, dass sich die Funktionen der API bei einem kostenlosen Account auf den lesenden Zugriff beschränkt (d.h. Readings über die aktuelle Wiedergabe).
Ich selbst habe nur einen Premium-Account (ist bei einem Familien-Abo mit 2,50€ p.P. ja vertretbar), kann also nicht sagen, welche Funktionen klappen und welche nicht.

LG
Modulentwickler
- Spotify #72490
- Nello #75127

jove01

O.k.
Das mit dem lesenden Zugriff könnte dann mit dem was ich sehe zutreffen.

Vielen Dank
Jürgen
Aktuelles FHEM auf Raspi 3 und dbLog
CUL 433
HMLan Rolladensteuerung

swilalaa

Danke für das super Plugin, die Einbindung von Spotify funktioniert ruck zuck!

Nun würde ich gerne homebridge / Siri eine fixe Playlist starten / pausieren. Die Lautstärke spielt nicht wirklich eine Rolle, wäre nur ein nice to have.

Ich habe bisher nur Lichter via Homebridge eingebunden, bin aber zu doof die Brücke zu schlagen wie ich die attribute ordentlich setze.

Könnte hier eventuell jemand unterstützen?

Danke im voraus!