From f112c9977963edfca2ddba6de2ef83f0d8979cf4 Mon Sep 17 00:00:00 2001 From: Chris Xiong Date: Sun, 5 Dec 2021 14:56:02 +0800 Subject: Roll Pineapple Cloud Music to c1dd09c. --- libs/music/player.d/cgi-bin/pcm.cgi | 76 +++++++++----- libs/music/player.d/pcm.js | 200 +++++++++++++++++++++++++----------- libs/music/player.d/progress-bar.js | 97 +++++++++++++++++ 3 files changed, 284 insertions(+), 89 deletions(-) create mode 100644 libs/music/player.d/progress-bar.js (limited to 'libs/music/player.d') diff --git a/libs/music/player.d/cgi-bin/pcm.cgi b/libs/music/player.d/cgi-bin/pcm.cgi index 2d5eaca..a1dc27f 100755 --- a/libs/music/player.d/cgi-bin/pcm.cgi +++ b/libs/music/player.d/cgi-bin/pcm.cgi @@ -1,34 +1,54 @@ #!/usr/bin/python3 import sys,os,cgi,json from urllib.parse import quote,unquote -d=cgi.parse(fp=sys.stdin) -print('Status: 200 OK',end='\r\n') -print('Content-type: application/json',end='\r\n') -print(end='\r\n') +def getfilelist(d): + fmt='ogg' if 'fmt' not in d else d['fmt'] -fmt='ogg' if 'fmt' not in d else d['fmt'] + ro={'status':200,'message':'OK'} + if 'folder' not in d or d['folder']=='': + plp=os.environ['DOCUMENT_ROOT']+'/libs/music/player.d/playlists/playlists' + alblist=list() + with open(plp,mode='r',encoding='utf-8') as f: + for line in f: + line=line.strip() + if len(line)>0:alblist.append(quote(line)) + rro={'type':'fileList','data':{'subFolderList':alblist}} + ro['result']=rro + print(json.dumps(ro)) + else: + alp=os.environ['DOCUMENT_ROOT']+'/libs/music/player.d/playlists/'+unquote(d['folder'][0]).strip('/')+'.playlist' + alblist=list() + with open(alp,mode='r',encoding='utf-8') as f: + for line in f: + line=line.strip() + if len(line)>0: + #hard code this for now, until pcm api stabilizes + alblist.append({'fileName':quote(line+'.ogg'),'fileSize':0,'modifiedTime':0}) + rro={'type':'fileList','data':{'musicList':alblist[1:],'subFolderList':list()}} + ro['result']=rro + print(json.dumps(ro)) + +def getserverinfo(d): + ro={'status': 200, 'message': 'OK'} + ro['result']={ + 'serverName': 'Pineapple Cloud Music', + 'serverShortName': 'PCM', + 'baseFolderNameHint': '', + 'preferredFormatsHint': 'ogg', + 'apiVersion': 1, + 'mediaRootUrl': '//filestorage.chrisoft.org/music/ogg/' + } + print(json.dumps(ro)) + +if __name__ == '__main__': + d=cgi.parse(fp=sys.stdin) + + print('Status: 200 OK',end='\r\n') + print('Content-type: application/json',end='\r\n') + print(end='\r\n') + + actionmap={'getserverinfo':getserverinfo, 'getfilelist':getfilelist} + if d['do'][0] in actionmap: + actionmap[d['do'][0]](d) -ro={'status':200,'message':'OK'} -if 'folder' not in d or d['folder']=='': - plp=os.environ['DOCUMENT_ROOT']+'/libs/music/player.d/playlists/playlists' - alblist=list() - with open(plp,mode='r',encoding='utf-8') as f: - for line in f: - line=line.strip() - if len(line)>0:alblist.append(quote(line)) - rro={'type':'fileList','data':{'subFolderList':alblist}} - ro['result']=rro - print(json.dumps(ro)) -else: - alp=os.environ['DOCUMENT_ROOT']+'/libs/music/player.d/playlists/'+unquote(d['folder'][0]).strip('/')+'.playlist' - alblist=list() - with open(alp,mode='r',encoding='utf-8') as f: - for line in f: - line=line.strip() - if len(line)>0: -#hard code this for now, until pcm api stabilizes - alblist.append({'fileName':quote(line+'.ogg'),'fileSize':0,'modifiedTime':0}) - rro={'type':'fileList','data':{'musicList':alblist[1:],'subFolderList':list()}} - ro['result']=rro - print(json.dumps(ro)) diff --git a/libs/music/player.d/pcm.js b/libs/music/player.d/pcm.js index 0efe849..6e2022c 100644 --- a/libs/music/player.d/pcm.js +++ b/libs/music/player.d/pcm.js @@ -1,11 +1,8 @@ -; // Private Cloud Music - player.js -; // Licence: WTFPL -; // BLumia - 2016/11/11 +; // SPDX-FileCopyrightText: 2021 Gary Wang +; // SPDX-License-Identifier: MIT ; // szO Chris && 2jjy && jxpxxzj Orz ; // ↑ Moe ↑ Moe ↑ Moe -; // Modified to use on chrisoft.org by Chris Xiong -; // szO BLumia Orz -; // ↑ Moe + // formatTime,getCookie by Chrissssss function formatTime(t) { if(isNaN(t))return '--:--'; @@ -24,9 +21,9 @@ function setCookie(cookieName, cookieValue, maxAge = 0) { if (maxAge > 0) cookieStr += ";max-age=" + maxAge; document.cookie = cookieStr; } - -const PCMAPI_URL='/libs/music/player.d/cgi-bin/pcm.cgi'; -const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; +function displayName(item) { + return item.displayName ? item.displayName : decodeURIComponent(item.fileName); +} (function() { var Helper = function() { @@ -49,8 +46,16 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; if(this.el) this.el.style.cssText += ';' + property + ":" + value; return this; }, - attr: function(property, value) { - if(this.el) this.el.setAttribute(property, value); + attr: function(attr, value) { + if(this.el) this.el.setAttribute(attr, value); + return this; + }, + removeData: function(attr) { + if(this.el) this.el.removeAttribute("data-" + attr); + return this; + }, + data: function(attr, value) { + if(this.el) this.el.setAttribute("data-" + attr, JSON.stringify(value)); return this; }, append: function(node) { @@ -76,9 +81,17 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; var f = new Helper(); return f.entry(selector); } + function TrickOrTreat(promiseRsp) { + if (!promiseRsp.ok) { + throw Error(promiseRsp.statusText); // to cancel the Promise chain... + } + return promiseRsp.json(); + } var Player = { + mediaRootUrl: '', path: null, // sample: 'Test/' data: null, + preferredFormats: undefined, // sample: 'mp3,ogg' audio: document.getElementsByTagName('audio')[0], currentIndex: -1, loop: 0, @@ -86,6 +99,14 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; playlist: H("playlist").el, folderlist: H("folderlist").el, nowPlaying: H("nowPlaying").el, + apiUrl: "/libs/music/player.d/cgi-bin/pcm.cgi", + _currentSongInfoJson: undefined, + _chapterNeedUpdate: true, + + setInfoJson: (jsonData) => { + this._currentSongInfoJson = jsonData; + this._chapterNeedUpdate = true; + }, updateMetadata: function() { if ('mediaSession' in navigator) { @@ -96,25 +117,90 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; } }, + applyChapterData: () => { + if (!this._chapterNeedUpdate) return; + if (Player.audio.duration) { + if (this._currentSongInfoJson) { + let duration = Player.audio.duration; + let progressChapterData = []; + this._currentSongInfoJson.chapters.forEach((chapter) => { + let chapterObj = {}; + chapterObj.start = chapter.start_time / duration * 100; + chapterObj.title = chapter.title; + progressChapterData.push(chapterObj); + }); + H("progress-bar").data("chapters", progressChapterData); + } else { + H("progress-bar").removeData("chapters"); + } + this._chapterNeedUpdate = false; + } + }, + + fetchAdditionalInfo: (infoJsonfileUrl) => { + fetch(infoJsonfileUrl).then(TrickOrTreat).then((data) => { + Player.setInfoJson(data); + }); + }, + playAtIndex: function(i) { + let fullPath = this.path + this.data[i].fileName; + let srcUrl = this.data[i].url ? this.data[i].url : (this.mediaRootUrl + fullPath); // FIXME: trigger this when audio doesn't finished load will cause play promise error. this.audio.pause(); this.currentIndex = i; - this.audio.src = AUDIO_URL + this.data[i].fileName; + this.audio.src = srcUrl; this.audio.load(); this.audio.play(); - window.history.replaceState("","Useless Title","#/"+this.path+this.data[i].fileName+"/"); // title seems be fucked. - H(this.nowPlaying).innerHTML(decodeURIComponent(this.data[i].fileName)); + window.history.replaceState("","Useless Title","#/" + fullPath + "/"); // title seems be fucked. + H(this.nowPlaying).innerHTML(displayName(this.data[i])); + + if (this.data[i].additionalInfo) { + let infoJsonFile = (fullPath.substring(0, fullPath.lastIndexOf('.')) || fullPath) + ".info.json"; + this.fetchAdditionalInfo(infoJsonFile); + } else { + this.setInfoJson(undefined); + } + }, + + fetchServerInfo: function(callback) { + var that = this; + fetch(this.apiUrl, { + method: 'POST', + body: new URLSearchParams({ + 'do': 'getserverinfo' + }) + }).then(TrickOrTreat).then((data) => { + if (data.result.mediaRootUrl && data.result.mediaRootUrl.length > 1) { + that.mediaRootUrl = data.result.mediaRootUrl; + if (!that.mediaRootUrl.endsWith('/')) { + that.mediaRootUrl = that.mediaRootUrl + '/'; + } + } + if (data.result.serverName) { + let el = H("server-name"); + if (el) { + el.text(data.result.serverName); + } + document.title = data.result.serverName; + } + + typeof callback === 'function' && callback(); + }) }, freshFolderlist: function(callback) { - var xhr = new XMLHttpRequest(); - xhr.open("POST", PCMAPI_URL, true); - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); var that = this; - xhr.onreadystatechange = function () { - if (xhr.readyState != 4 || xhr.status != 200) return; - var data = JSON.parse(xhr.responseText); + requestBody = { + 'do': 'getfilelist', + }; + if (that.preferredFormats) { + requestBody['preferredFormats'] = that.preferredFormats; + } + fetch(this.apiUrl, { + method: 'POST', + body: new URLSearchParams(requestBody) + }).then(TrickOrTreat).then((data) => { if (data.status != 200) { console.error("Fetch error. Reason: " + data.message + " Url: ./api.php"); return; @@ -129,11 +215,7 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; ).el ); }); - }; - xhr.onerror = function() { - console.error("Ajax load folders failed. Status: " + xhr.status + " Url: ./api.php"); - }; - xhr.onloadend = function() { + var nodeList = document.querySelectorAll('#folderlist a'); for(var i = 0; i < nodeList.length; i++) { var el = nodeList[i]; @@ -142,41 +224,34 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; that.fetchData(); }; } + typeof callback === 'function' && callback(); - } - xhr.send("do=getfilelist"); + }); }, fetchData: function() { var that = this; - var xhr = new XMLHttpRequest(); - xhr.open("POST", PCMAPI_URL, true); - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xhr.onreadystatechange = function () { - if (xhr.readyState != 4 || xhr.status != 200) return; - var data = JSON.parse(xhr.responseText); - if (data.status != 200) { - console.error("Fetch error. Reason: " + data.message + " Url: ./api.php"); - return; - } + + fetch(this.apiUrl, { + method: 'POST', + body: new URLSearchParams({ + 'do': 'getfilelist', + 'folder': that.path + }) + }).then(TrickOrTreat).then((data) => { that.data = data.result.data.musicList; that.freshPlaylist(); that.freshSubFolderList(data.result.data.subFolderList); - }; - xhr.onerror = function() { - console.error("Ajax load playlist failed. Status: " + xhr.status + " Url: ./api.php"); - that.data = []; - }; - xhr.send("do=getfilelist&folder="+that.path); + }); }, - + freshPlaylist : function() { var that = this; var data = this.data; var songTitle = ''; this.playlist.innerHTML = ''; data.forEach(function(item, i) { - songTitle = decodeURIComponent(item.fileName); + songTitle = displayName(item); H(that.playlist).append( H("").attr('index', i).append( H("
  • ").text(songTitle).el @@ -218,13 +293,13 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; urlMatch : function() { var isUrlMatched = false; // Match folder name and song title. - var re = new RegExp("[#][/](.*[/])(.*)[/]$"); + var re = new RegExp("[#][/](.*[/])(.*.[a-zA-z0-9]{1,3})[/]"); var urlMatch = re.exec(location.href); if (urlMatch != null) { isUrlMatched = true; this.path = urlMatch[1]; - this.audio.src = AUDIO_URL + urlMatch[2] + '.ogg'; - this.audio.play(); + this.audio.src = (this.path + urlMatch[2]); + this.audio.play().catch((reason) => { console.log(reason); }); H(this.nowPlaying).innerHTML(decodeURIComponent(urlMatch[2])); } // Only match folder name. @@ -261,30 +336,33 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; H("btn-order").innerHTML("Order: ×"); } }, - + init : function() { var that = this; - this.freshFolderlist(function() { - that.urlMatch(); - that.fetchData(); + this.fetchServerInfo(function() { + that.freshFolderlist(function() { + that.urlMatch(); + that.fetchData(); + }); }); this.loop = getCookie("pcm-loop") == "1" ? 1 : 0; this.order = getCookie("pcm-order") == "1" ? 1 : 0; this.applyLoop(); this.applyOrder(); }, - + ready : function() { var that = this; - this.audio.ontimeupdate = function() { + this.audio.ontimeupdate = () => { + this.applyChapterData(); H("curTime").innerHTML(formatTime(Player.audio.currentTime)); H("totalTime").innerHTML(formatTime(Player.audio.duration)); - H("timebar").css("width", Player.audio.currentTime / Player.audio.duration*100+"%"); + H("progress-bar").attr("value", Player.audio.currentTime / Player.audio.duration*100); var r = 0; - for(var i=0; i +; // SPDX-License-Identifier: MIT +class ProgressBar extends HTMLElement { + + static get observedAttributes() { + return ['value', 'buffer', 'data-chapters']; + } + + constructor() { + super(); // always call super() first in the constructor. + const shadow = this.attachShadow({mode: 'open'}); + + const container = document.createElement('div'); + container.setAttribute('class', 'container'); + + const bufferBar = document.createElement('div'); + bufferBar.setAttribute('id', 'bufferbar'); + + const timeBar = document.createElement('div'); + timeBar.setAttribute('id', 'timebar'); + + const chapterContainer = document.createElement('div'); + chapterContainer.setAttribute('id', 'chapter-container'); + + const style = document.createElement('style'); + style.textContent = ` + .container { + height: 1.5em; + position: relative; + background-color: #f1f1f1; + } + .container > div { + height: 100%; + position: absolute; + } + #timebar { + background-color: #2196F3; + } + #bufferbar { + background-color: #AAA; + } + .container, #chapter-container { + width: 100%; + } + .chapter { + height: 100%; + width: stretch; width: -moz-available; width: -webkit-fill-available; + border-left: .15em solid #0045F340; + position: absolute; + } + `; + + shadow.appendChild(container); + shadow.appendChild(style); + container.appendChild(bufferBar); + container.appendChild(timeBar); + container.appendChild(chapterContainer); + } + + connectedCallback() { + spawnChapters(this); + updateStyle(this); + }; + + attributeChangedCallback(name, oldValue, newValue) { + if (name == "data-chapters") { + spawnChapters(this); + } + updateStyle(this); + } +} + +function spawnChapters(elem) { + const shadow = elem.shadowRoot; + let chapterContainer = shadow.querySelector('#chapter-container'); + let chapters = elem.dataset.chapters ? JSON.parse(elem.dataset.chapters) : []; + + if (!Array.isArray(chapters)) return; + chapterContainer.textContent = ''; + chapters.forEach((chapter) => { + let chapterElem = document.createElement('div'); + chapterElem.setAttribute('class', 'chapter'); + chapterElem.setAttribute('title', `${chapter.title}`); + chapterElem.setAttribute('style', `left: ${chapter.start}%`); + chapterContainer.appendChild(chapterElem); + }); +} + +function updateStyle(elem) { + const shadow = elem.shadowRoot; + let timebar = shadow.querySelector('#timebar'); + timebar.setAttribute('style', `width: ${elem.getAttribute('value')}%`); + let bufferbar = shadow.querySelector('#bufferbar'); + bufferbar.setAttribute('style', `width: ${elem.getAttribute('buffer')}%`); +} + +customElements.define('pcm-progress', ProgressBar); -- cgit v1.2.3