Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Show radio stream title using php and rest api #992

Merged
merged 9 commits into from
Jun 29, 2022
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
['name' => 'radioApi#exportAllToFile', 'url' => '/api/radio/export', 'verb' => 'POST'],
['name' => 'radioApi#importFromFile', 'url' => '/api/radio/import', 'verb' => 'POST'],
['name' => 'radioApi#resetAll', 'url' => '/api/radio/reset', 'verb' => 'POST'],
['name' => 'radioApi#getRadioStreamData', 'url' => '/api/radio/{id}/info', 'verb' => 'GET'],

// podcast API
['name' => 'podcastApi#getAll', 'url' => '/api/podcasts', 'verb' => 'GET'],
Expand Down
45 changes: 41 additions & 4 deletions js/app/controllers/playercontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function ($scope, $rootScope, playlistService, Audio, gettextCatalog, Restangula
total: 0
};
var scrobblePending = false;
var timeoutId = 0;

// shuffle and repeat may be overridden with URL parameters
if ($location.search().shuffle !== undefined) {
Expand Down Expand Up @@ -104,6 +105,13 @@ function ($scope, $rootScope, playlistService, Audio, gettextCatalog, Restangula
});
onPlayerEvent('play', function() {
$rootScope.playing = true;
if (timeoutId != 0) {
clearTimeout(timeoutId);
timeoutId = 0;
}
if (currentTrackIsStream()) {
$scope.getStreamTitle();
}
});
onPlayerEvent('pause', function() {
$rootScope.playing = false;
Expand Down Expand Up @@ -438,7 +446,7 @@ function ($scope, $rootScope, playlistService, Audio, gettextCatalog, Restangula
};

$scope.secondaryTitle = function() {
return $scope.currentTrack?.artistName ?? $scope.currentTrack?.channel?.title ?? $scope.currentTrack?.stream_url ?? null;
return $scope.currentTrack?.artistName ?? $scope.currentTrack?.channel?.title ?? $scope.currentTrack?.currentTitle ?? $scope.currentTrack?.stream_url ?? null;
};

$scope.coverArt = function() {
Expand Down Expand Up @@ -498,6 +506,35 @@ function ($scope, $rootScope, playlistService, Audio, gettextCatalog, Restangula
return command + ' (' + cmdScope + ')';
};

$scope.onMetadata = function(streamTitle) {
console.log('MetaData recieved: ' + streamTitle);
if ((streamTitle) && $scope.currentTrack.currentTitle !== streamTitle) {
$scope.currentTrack.currentTitle = streamTitle;
}
};

$scope.getStreamTitle = function() {
if ($rootScope.playing) {
var currentTrackId = $scope.currentTrack.id;
Restangular.one('radio', $scope.currentTrack.id).one('info').get().then(
function(response) {
if ($scope.currentTrack && $scope.currentTrack.id == currentTrackId) {
$scope.onMetadata(response.title);
}
},
function(_error) {
// error handling
if ($scope.currentTrack && $scope.currentTrack.id == currentTrackId) {
$scope.onMetadata($scope.currentTrack.stream_url);
}
}
);
timeoutId = setTimeout(() => $scope.getStreamTitle(), 32000);
} else {
timeoutId = 0;
}
};

/**
* The coverArtToken is used to enable loading the cover art in the mediaSession of Firefox. There,
* the loading happens in a context where the normal session cookies are not available. Hence, for the
Expand Down Expand Up @@ -539,7 +576,7 @@ function ($scope, $rootScope, playlistService, Audio, gettextCatalog, Restangula
if (track.type === 'radio') {
navigator.mediaSession.metadata = new MediaMetadata({
title: track.name,
artist: track.stream_url,
artist: track.currentTitle ?? track.stream_url,
artwork: [{
sizes: '190x190',
src: radioIconPath,
Expand All @@ -560,7 +597,7 @@ function ($scope, $rootScope, playlistService, Audio, gettextCatalog, Restangula
});
}
}
});
}, true);
}

/**
Expand Down Expand Up @@ -606,6 +643,6 @@ function ($scope, $rootScope, playlistService, Audio, gettextCatalog, Restangula
notification = null;
}
}
});
}, true);
}
}]);
24 changes: 24 additions & 0 deletions lib/Controller/RadioApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\DataResponse;

use OCP\Files\Folder;
use OCP\IRequest;
Expand All @@ -25,6 +26,7 @@
use OCA\Music\Http\ErrorResponse;
use OCA\Music\Utility\PlaylistFileService;
use OCA\Music\Utility\Util;
use OCA\Music\Utility\RadioMetadata;

class RadioApiController extends Controller {
private $businessLayer;
Expand Down Expand Up @@ -201,4 +203,26 @@ public function resetAll() {
$this->businessLayer->deleteAll($this->userId);
return new JSONResponse(['success' => true]);
}

/**
* get radio metadata from stream
*
* @NoAdminRequired
* @NoCSRFRequired
*/

public function getRadioStreamData(int $id) {
try {
$response = "";
$station = $this->businessLayer->find($id, $this->userId);
$stapi = $station->toAPI();
paulijar marked this conversation as resolved.
Show resolved Hide resolved
if (isset($stapi['stream_url'])) {
$response = RadioMetadata::fetchStreamData($stapi['stream_url'], 1, 1);
}
return new DataResponse([ 'title' => $response ]);

} catch (BusinessLayerException $ex) {
return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
}
}
}
135 changes: 135 additions & 0 deletions lib/Utility/RadioMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php declare(strict_types=1);

/**
* ownCloud - Music app
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Moahmed-Ismail MEJRI <imejri@hotmail.com>
* @copyright Moahmed-Ismail MEJRI 2022
*/

namespace OCA\Music\Utility;

use OCA\Music\Utility\Util;

/**
* MetaData radio utility functions
*/
class RadioMetadata {

private static function findStr($data, $str) {
$ret = "";
foreach ($data as $value) {
$find = strstr($value, $str);
if ($find !== false) {
$ret = $find;
break;
}
}
return $ret;
}

private static function parseStreamUrl($url) {

$ret = array();
$parse_url = parse_url($url);

$ret['port'] = 80;
if (isset($parse_url['port'])) {
$ret['port'] = $parse_url['port'];
} else if ($parse_url['scheme'] == "https") {
$ret['port'] = 443;
}

$ret['scheme'] = $parse_url['scheme'];
$ret['hostname'] = $parse_url['host'];
$ret['pathname'] = $parse_url['path'];

if (isset($parse_url['query'])) {
$ret['pathname'] .= "?" . $parse_url['query'];
}

if ($parse_url['scheme'] == "https") {
$ret['sockAddress'] = "ssl://" . $ret['hostname'];
} else {
$ret['sockAddress'] = $ret['hostname'];
}

return $ret;
}


public static function fetchUrlData($url) : array {
$title = "";
$content = \file_get_contents($url);
list($version, $status_code, $msg) = explode(' ', $http_response_header[0], 3);
if ($status_code == 200) {
$data = explode(',', $content);
$title = $data[6];
}
return $title;
}

public static function fetchStreamData($url, $maxattempts, $maxredirect) {
$timeout = 10;
$streamTitle = "";
$pUrl = self::parseStreamUrl($url);
if (($pUrl['sockAddress']) && ($pUrl['port'])) {
$fp = fsockopen($pUrl['sockAddress'], $pUrl['port'], $errno, $errstr, $timeout);
if ($fp != false) {
$out = "GET " . $pUrl['pathname'] . " HTTP/1.1\r\n";
$out .= "Host: ". $pUrl['hostname'] . "\r\n";
$out .= "Accept: */*\r\n";
$out .= "User-Agent: OCMusic/1.52\r\n";
$out .= "Icy-MetaData: 1\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
stream_set_timeout($fp, $timeout);

$header = fread($fp, 1024);
$headers = explode("\n", $header);

if (strpos($headers[0], "200 OK") !== false) {
$interval = 0;
$line = self::findStr($headers, "icy-metaint:");
if ($line) {
$interval = trim(explode(':', $line)[1]);
}

if (($interval)&&($interval<64001)) {
$attempts = 0;
while ($attempts < $maxattempts) {
for ($j = 0; $j < $interval; $j++) {
fread($fp, 1);
}

$meta_length = ord(fread($fp, 1)) * 16;
if ($meta_length) {
$metadatas = explode(';', fread($fp, $meta_length));
$metadata = self::findStr($metadatas, "StreamTitle");
if ($metadata) {
$streamTitle = Util::truncate(trim(explode('=', $metadata)[1], "'"), 256);
break;
}
}
$attempts++;
}
} else {
$streamTitle = RadioMetadata::fetchUrlData($pUrl['scheme'] . '://' . $pUrl['hostname'] . ':' . $pUrl['port'] . '/7.html');
}
} else if (($maxredirect>0)&&(strpos($headers[0], "302 Found") !== false)) {
$value = self::findStr($headers, "Location:");
if ($value) {
$location = trim(substr($value, 10), "\r");
$streamTitle = RadioMetadata::fetchStreamData($location, $maxattempts, $maxredirect-1);
}
}
fclose($fp);
}
}
return $streamTitle;
}

}