Lyrics plugin for Synology Audio Station/DS Audio
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

373 lines
12 KiB

<?php
// Must set to false when running on DS Station
const DEBUG = false;
// Set to TURE is Chinese translation is needed
const NEED_TRANSLATION = false;
/**
* Implements the functions required by Audio Station/DSAudio.
* <p>
* Features:
* - Sort result according to similarity of artist and title.
* - Add Chinese translated lyric if {@code NEED_TRANSLATION} is {@code TRUE}.
*
* @author Ludy Su (https://github.com/LudySu/Synology-LrcPlugin)
* @see https://global.download.synology.com/download/Document/DeveloperGuide/AS_Guide.pdf
*/
class LudysuNetEaseLrc {
private $mArtist = "";
private $mTitle = "";
///////////////////////////// Synology API ///////////////////////////////////////
/**
* Searches for a song with the artist and title, and returns the matching result list. Result is sorted based on similarity of artist and title.
*/
public function getLyricsList($artist, $title, $info) {
$artist = trim($artist);
$title = trim($title);
$this->mArtist = $artist;
$this->mTitle = $title;
if ($this->isNullOrEmptyString($title)) {
return 0;
}
$response = $this->search($title);
if ($this->isNullOrEmptyString($response)) {
return 0;
}
$json = json_decode($response, true);
$songArray = $json['result']['songs'];
if(count($songArray) == 0) {
return 0;
}
// Try to find the titles that match exactly
$exactMatchArray = array();
$partialMatchArray = array();
foreach ($songArray as $song) {
$lowTitle = strtolower($title);
$lowResult = strtolower($song['name']);
if (strtolower($lowTitle) === strtolower($lowResult)) {
array_push($exactMatchArray, $song);
} else if (strpos($lowResult, $lowTitle) !== FALSE || strpos($lowTitle, $lowResult) !== FALSE) {
array_push($partialMatchArray, $song);
}
}
if (count($exactMatchArray) != 0) {
$songArray = $exactMatchArray;
} else if (count($partialMatchArray) != 0) {
$songArray = $partialMatchArray;
}
// Get information from songs
$foundArray = array();
foreach ($songArray as $song) {
$elem = array(
'id' => $song['id'],
'artist' => $song['artists'][0]["name"],
'title' => $song['name'],
'alt' => $song['alias'][0] . "; Album: " . $song['album']['name']
);
// Find the best match artist from all artists belong to a song
$max = 0;
foreach ($song['artists'] as $item) {
$score = $this->getStringSimilarity($artist, $item['name']);
if ($score > $max) {
$max = $score;
$elem['artist'] = $item['name'];
}
}
array_push($foundArray, $elem);
}
// Sort by best match according to similarity of artist and title
usort($foundArray, array($this,'cmp'));
foreach ($foundArray as $song) {
// add artist, title, id, lrc preview (or additional comment)
$info->addTrackInfoToList($song['artist'], $song['title'], $song['id'], $song['id'] . "; " . $song['alt']);
}
return count($foundArray);
}
/**
* Downloads a lyric with the specific ID. Will have Chinese translation if {@code NEED_TRANSLATION} is {@code TRUE}.
*/
public function getLyrics($id, $info) {
$lrc = $this->downloadLyric($id);
if ($this->isNullOrEmptyString($lrc)) {
return FALSE;
}
$info->addLyrics($lrc, $id);
return true;
}
///////////////////////////// Utils ///////////////////////////////////////
/**
* Gets all lyrics, apart from original one, translated and karaoke versions will also be returned if available.
*/
private function downloadLyric($music_id) {
$response = $this->download($music_id);
if ($this->isNullOrEmptyString($response)) {
return NULL;
}
$json = json_decode($response, true);
$orgLrc = $json['lrc']['lyric'];
$transLrc = $json['tlyric']['lyric']; // Chinese translation lyric, but only some songs have
$resultLrc = $orgLrc;
if (NEED_TRANSLATION && !$this->isNullOrEmptyString($transLrc)) {
$resultLrc = "";
$orgLines = $this->processLrcLine($orgLrc);
$transLines = $this->processLrcLine($transLrc);
$transCursor = 0;
foreach ($orgLines as $elem) {
$key = $elem['tag']; // time tag
$value = $elem['lrc']; // lyric line
$resultLrc .= $key . $value;
// Find matching translation
$trans = "";
if (!$this->isNullOrEmptyString($key)) {
$time = $this->getTimeFromTag($key);
for ($i = $transCursor; $i < count($transLines); $i++) {
$tKey = $transLines[$i]['tag'];
if ($this->getTimeFromTag($tKey) > $time) { // trans time tag is greater than org, no match found
$transCursor = $i;
break;
}
$tValue = $transLines[$i]['lrc'];
// Check for matching time tag
if ($key === $tKey) {
$transCursor = $i + 1;
$trans = $tValue;
break;
}
}
}
if (!$this->isNullOrEmptyString($trans)) { // $key is empty when it's not time tag, just metadata
$resultLrc .= "" . $trans . "";
}
$resultLrc .= "\n";
}
}
return $resultLrc;
}
// Comparator that determines which matches better
private function cmp($lhs, $rhs) {
$scoreArtistL = $this->getStringSimilarity($this->mArtist, $lhs['artist']);
$scoreArtistR = $this->getStringSimilarity($this->mArtist, $rhs['artist']);
$scoreTitleL = $this->getStringSimilarity($this->mTitle, $lhs['title']);
$scoreTitleR = $this->getStringSimilarity($this->mTitle, $rhs['title']);
// printf("artist " . $lhs['artist'] . " vs " . $rhs['artist'] . " | " . $scoreArtistL . " vs " . $scoreArtistR . "</br>");
// printf("title " . $lhs['title'] . " vs " . $rhs['title'] . " | " . $scoreTitleL . " vs " . $scoreTitleR. "</br>");
return $scoreArtistR + $scoreTitleR - $scoreArtistL - $scoreTitleL;
}
/**
* Gets similarity score of 0-100 between 2 strings, the bigger the score is, the more similarity.
*/
private static function getStringSimilarity($lhs, $rhs) {
similar_text($lhs, $rhs, $percent);
return $percent;
}
private function getTimeFromTag($tag) {
$min = substr($tag, 1, 2);
$sec = substr($tag, 4, 2);
$milli = substr($tag, 7, strlen($tag) - 8);
return $milli + $sec * 1000 + $min * 60 * 1000;
}
private function getLrcTag($str) {
$tag = strstr($str, "]", true);
if ($tag === false) {
return "";
}
return $tag . "]";
}
private function getLrcText($str, $tag) {
if ($this->isNullOrEmptyString($tag)) {
return $str;
}
return str_replace($tag, "", $str);
}
private function processLrcLine($lrc) {
$result = array();
foreach (explode("\n", $lrc) as $line) {
$key = $this->getLrcTag($line);
$value = $this->getLrcText($line, $key);
if (!$this->isValidLrcTime($key)) {
$key = "";
$value = $line;
}
array_push($result, array(
'tag' => $key,
'lrc' => $value
));
}
return $result;
}
private function isValidLrcTime($str) {
if ($this->isNullOrEmptyString($str) || $str[0] !== "[") {
return FALSE;
}
$len = strlen($str);
if ($len < 9 || $len > 11) {
return FALSE;
}
for ($count = 1; $count < $len - 1; $count++) {
$ch = $str[$count];
if ($ch !== ":" && $ch !== "." && !is_numeric($ch)) {
return FALSE;
}
}
return TRUE;
}
// Function for basic field validation (present and neither empty nor only white space
private static function isNullOrEmptyString($question){
return (!isset($question) || trim($question)==='');
}
///////////////////////////// Netease API ///////////////////////////////////////
/**
* Searches for a song based on title.
*/
private static function search($word) {
$params = array(
's' => $word,
'offset' => '0', 'limit' => '20',
'total' => true,
'type' => '1', //搜索单曲(1),歌手(100),专辑(10),歌单(1000),用户(1002)
);
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "http://music.163.com/api/search/get/web",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
));
$output = curl_exec($curl);
curl_close($curl);
return $output;
}
/**
* Downloads a lyric for a given music_id.
*/
private static function download($music_id) {
// lv = original version; tv = translated version; kv = karaoke version, rarely available. Set value to 0 if don't want
$url = "http://music.163.com/api/song/lyric?os=pc&id=" . $music_id . "&lv=-1&kv=0&tv=-1";
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
));
$output = curl_exec($curl);
curl_close($curl);
return $output;
}
} // End of class
////////////////////////// DEBUG ////////////////////////////////////
if (DEBUG == true) {
class TestObj {
private $items;
function __construct() {
$this->items = array();
}
public function addLyrics($lyric, $id) {
printf("</br>");
printf("song id: %s\n", $id);
printf("</br>");
printf("== lyric ==\n");
printf("%s\n", $lyric);
printf("** END of lyric **\n\n");
}
public function addTrackInfoToList($artist, $title, $id, $prefix) {
printf("</br>");
printf("song id: %s\n", $id);
printf("artist [%s]\n", $artist);
printf("title [%s]\n", $title);
printf("prefix [%s]\n", $prefix);
printf("</br>");
array_push($this->items, array(
'artist' => $artist,
'title' => $title,
'id' => $id
));
}
function getItems() {
return $this->items;
}
function getFirstItem() {
if (count($this->items) > 0) {
return $this->items[0];
} else {
return NULL;
}
}
}
/**
* Main
*/
$title = "tell your world";
$artist = "初音ミク";
echo "Trying to find lyrics for ['$title'] by artist ['$artist'] ...</br>";
$testObj = new TestObj();
$downloader = (new ReflectionClass("LudysuNetEaseLrc"))->newInstance();
$count = $downloader->getLyricsList($artist, $title, $testObj);
if ($count > 0) {
$item = $testObj->getFirstItem();
if (array_key_exists('id', $item)) {
$downloader->getLyrics($item['id'], $testObj);
} else {
echo "\nno id to query lyric\n";
}
} else {
echo " ****************************\n";
echo " *** Failed to find lyric ***\n";
echo " ****************************\n";
}
}