FirmwareHelper.pm 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package Plugins::SqueezeESP32::FirmwareHelper;
  2. use strict;
  3. use File::Basename qw(basename);
  4. use File::Spec::Functions qw(catfile);
  5. use JSON::XS::VersionOneAndTwo;
  6. use Slim::Utils::Log;
  7. use Slim::Utils::Prefs;
  8. use constant FIRMWARE_POLL_INTERVAL => 3600 * (5 + rand());
  9. use constant GITHUB_RELEASES_URI => "https://api.github.com/repos/sle118/squeezelite-esp32/releases";
  10. use constant GITHUB_ASSET_URI => GITHUB_RELEASES_URI . "/assets/";
  11. use constant GITHUB_DOWNLOAD_URI => "https://github.com/sle118/squeezelite-esp32/releases/download/";
  12. use constant ESP32_STATUS_URI => "/status.json";
  13. my $FW_DOWNLOAD_REGEX = qr|plugins/SqueezeESP32/firmware/([-a-z0-9-/.]+\.bin)$|i;
  14. my $FW_FILENAME_REGEX = qr/^squeezelite-esp32-.*\.bin(\.tmp)?$/;
  15. my $FW_TAG_REGEX = qr/\b(ESP32-A1S|SqueezeAmp|I2S-4MFlash)\.(16|32)\.(\d+)\.([-a-zA-Z0-9]+)\b/;
  16. my $prefs = preferences('plugin.squeezeesp32');
  17. my $log = logger('plugin.squeezeesp32');
  18. my $initialized;
  19. sub init {
  20. my ($client) = @_;
  21. if (!$initialized) {
  22. $initialized = 1;
  23. Slim::Web::Pages->addRawFunction($FW_DOWNLOAD_REGEX, \&handleFirmwareDownload);
  24. }
  25. # start checking for firmware updates
  26. Slim::Utils::Timers::setTimer($client, Time::HiRes::time() + 3.0 + rand(3.0), \&initFirmwareDownload);
  27. }
  28. sub initFirmwareDownload {
  29. my ($client) = @_;
  30. Slim::Utils::Timers::killTimers($client, \&initFirmwareDownload);
  31. Slim::Networking::SimpleAsyncHTTP->new(
  32. sub {
  33. my $http = shift;
  34. my $content = eval { from_json( $http->content ) };
  35. if ($content && ref $content) {
  36. my $releaseInfo = _getFirmwareTag($content->{version});
  37. if ($releaseInfo && ref $releaseInfo) {
  38. prefetchFirmware($releaseInfo);
  39. }
  40. }
  41. },
  42. sub {
  43. my ($http, $error) = @_;
  44. $log->error("Failed to get releases from Github: $error");
  45. },
  46. {
  47. timeout => 10
  48. }
  49. )->get('http://' . $client->ip . ESP32_STATUS_URI);
  50. Slim::Utils::Timers::setTimer($client, Time::HiRes::time() + FIRMWARE_POLL_INTERVAL, \&initFirmwareDownload);
  51. }
  52. sub prefetchFirmware {
  53. my ($releaseInfo) = @_;
  54. return unless $releaseInfo;
  55. Slim::Networking::SimpleAsyncHTTP->new(
  56. sub {
  57. my $http = shift;
  58. my $content = eval { from_json( $http->content ) };
  59. if (!$content || !ref $content) {
  60. $@ && $log->error("Failed to parse response: $@");
  61. }
  62. my $regex = $releaseInfo->{model} . '\.' . $releaseInfo->{res} . '\.\d+\.' . $releaseInfo->{branch};
  63. my $url;
  64. foreach (@$content) {
  65. if ($_->{tag_name} =~ /$regex/ && $_->{assets} && ref $_->{assets}) {
  66. ($url) = grep /\.bin$/, map {
  67. $_->{browser_download_url}
  68. } @{$_->{assets}};
  69. last if $url;
  70. }
  71. }
  72. downloadFirmwareFile(sub {
  73. main::INFOLOG && $log->is_info && $log->info("Pre-cached firmware file: " . $_[0]);
  74. }, sub {
  75. my ($http, $error, $url, $code) = @_;
  76. $error ||= ($http && $http->error) || 'unknown error';
  77. $url ||= ($http && $http->url) || 'no URL';
  78. $log->error(sprintf("Failed to get firmware image from Github: %s (%s)", $error || $http->error, $url));
  79. }, $url) if $url && $url =~ /^https?/;
  80. },
  81. sub {
  82. my ($http, $error) = @_;
  83. $log->error("Failed to get releases from Github: $error");
  84. },
  85. {
  86. timeout => 10,
  87. cache => 1,
  88. expires => 3600
  89. }
  90. )->get(GITHUB_RELEASES_URI);
  91. }
  92. sub handleFirmwareDownload {
  93. my ($httpClient, $response) = @_;
  94. my $request = $response->request;
  95. my $_errorDownloading = sub {
  96. _errorDownloading($httpClient, $response, @_);
  97. };
  98. my $path;
  99. if (!defined $request || !(($path) = $request->uri =~ $FW_DOWNLOAD_REGEX)) {
  100. return $_errorDownloading->(undef, 'Invalid request', $request->uri, 400);
  101. }
  102. main::INFOLOG && $log->is_info && $log->info("Requesting firmware from: $path");
  103. downloadFirmwareFile(sub {
  104. my $firmwareFile = shift;
  105. $response->code(200);
  106. Slim::Web::HTTP::sendStreamingFile($httpClient, $response, 'application/octet-stream', $firmwareFile, undef, 1);
  107. }, $_errorDownloading, GITHUB_DOWNLOAD_URI . $path);
  108. }
  109. sub downloadFirmwareFile {
  110. my ($cb, $ecb, $url, $name) = @_;
  111. # keep track of the last firmware we requested, to prefetch it in the future
  112. my $releaseInfo = _getFirmwareTag($url);
  113. $name ||= basename($url);
  114. if ($name !~ $FW_FILENAME_REGEX) {
  115. return $ecb->(undef, 'Unexpected firmware image name: ' . $name, $url, 400);
  116. }
  117. my $updatesDir = Slim::Utils::OSDetect::dirsFor('updates');
  118. my $firmwareFile = catfile($updatesDir, $name);
  119. my $fileMatchRegex = join('-', '', $releaseInfo->{branch}, $releaseInfo->{model}, $releaseInfo->{res});
  120. Slim::Utils::Misc::deleteFiles($updatesDir, $fileMatchRegex, $firmwareFile);
  121. if (-f $firmwareFile) {
  122. main::INFOLOG && $log->is_info && $log->info("Found cached firmware file");
  123. return $cb->($firmwareFile);
  124. }
  125. Slim::Networking::SimpleAsyncHTTP->new(
  126. sub {
  127. my $http = shift;
  128. if ($http->code != 200 || !-e "$firmwareFile.tmp") {
  129. return $ecb->($http, $http->mess);
  130. }
  131. rename "$firmwareFile.tmp", $firmwareFile or return $ecb->($http, "Unable to rename temporary $firmwareFile file" );
  132. return $cb->($firmwareFile);
  133. },
  134. $ecb,
  135. {
  136. saveAs => "$firmwareFile.tmp",
  137. }
  138. )->get($url);
  139. return;
  140. }
  141. sub _getFirmwareTag {
  142. my ($info) = @_;
  143. if (my ($model, $resolution, $version, $branch) = $info =~ $FW_TAG_REGEX) {
  144. my $releaseInfo = {
  145. model => $model,
  146. res => $resolution,
  147. version => $version,
  148. branch => $branch
  149. };
  150. return $releaseInfo;
  151. }
  152. }
  153. sub _errorDownloading {
  154. my ($httpClient, $response, $http, $error, $url, $code) = @_;
  155. $error ||= ($http && $http->error) || 'unknown error';
  156. $url ||= ($http && $http->url) || 'no URL';
  157. $code ||= ($http && $http->code) || 500;
  158. $log->error(sprintf("Failed to get data from Github: %s (%s)", $error || $http->error, $url));
  159. $response->headers->remove_content_headers;
  160. $response->code($code);
  161. $response->content_type('text/plain');
  162. $response->header('Connection' => 'close');
  163. $response->content('');
  164. $httpClient->send_response($response);
  165. Slim::Web::HTTP::closeHTTPSocket($httpClient);
  166. };
  167. 1;