diff options
author | Chris Xiong <chirs241097@gmail.com> | 2021-12-05 14:56:02 +0800 |
---|---|---|
committer | Chris Xiong <chirs241097@gmail.com> | 2021-12-05 14:56:02 +0800 |
commit | f112c9977963edfca2ddba6de2ef83f0d8979cf4 (patch) | |
tree | f32be24d1e88f33347de34326e65958540a7d970 /libs | |
parent | 7e8a73b251eaa4f7ea33c2d070620dad6242fc97 (diff) | |
download | web-f112c9977963edfca2ddba6de2ef83f0d8979cf4.tar.xz |
Roll Pineapple Cloud Music to c1dd09c.
Diffstat (limited to 'libs')
-rw-r--r-- | libs/music/pcm | 18 | ||||
-rwxr-xr-x | libs/music/player.d/cgi-bin/pcm.cgi | 76 | ||||
-rw-r--r-- | libs/music/player.d/pcm.js | 200 | ||||
-rw-r--r-- | libs/music/player.d/progress-bar.js | 97 |
4 files changed, 296 insertions, 95 deletions
diff --git a/libs/music/pcm b/libs/music/pcm index adb3458..7152edf 100644 --- a/libs/music/pcm +++ b/libs/music/pcm @@ -6,6 +6,14 @@ <meta name="theme-color" content="#f44336"> <link rel="stylesheet" href="/libs/music/player.d/w3.css"> <style> + @font-face{ + font-family: 'CMU Typewriter Text w'; + src: url(/cmunbtl.woff); + } + body, h1, h2, h3, h4 { + font-family: 'CMU Typewriter Text w', 'CMU Typewriter Text', 'TeX Gyre Cursor', 'FreeMono', 'Courier New', Courier, monospace; + font-variant-ligatures: none; + } .w3-sidenav ul li {padding-left: 2px;} #playlist li {word-break: break-all;} .ellipsis {overflow:hidden; text-overflow:ellipsis; white-space:nowrap;} @@ -52,7 +60,7 @@ <nav class="w3-sidenav w3-collapse w3-light-grey w3-animate-left w3-card-2" style="z-index: 3; width: 250px; display: none;" id="mySidenav"> <header class="w3-container w3-dark-grey"> - <h2>Albums <a href="javascript:void(0)" onclick="w3_close()" class="w3-right w3-xlarge w3-hide-large w3-closenav" title="close sidenav">×</a></h2> + <h2>Folders <a href="javascript:void(0)" onclick="w3_close()" class="w3-right w3-xlarge w3-hide-large w3-closenav" title="close sidenav">×</a></h2> </header> <ul class="w3-ul" style="margin-bottom: 120px;" id="folderlist"> </ul> @@ -60,7 +68,7 @@ <div class="w3-overlay w3-hide-large w3-animate-opacity" onclick="w3_close()" style="cursor: pointer; display: none;" id="myOverlay"></div> <div class="w3-main" style="margin-left:250px;"> <header class="w3-container w3-red w3-top"> - <h2 class="ellipsis"><span class="w3-opennav w3-xlarge w3-left w3-hide-large" onclick="w3_open()" id="openNav">☰</span> Pineapple Cloud Music</h2> + <h2 class="ellipsis"><span class="w3-opennav w3-xlarge w3-left w3-hide-large" onclick="w3_open()" id="openNav">☰</span> <span id="server-name">Private Cloud Music</span></h2> </header> <header class="w3-container w3-yellow"><h2 class="ellipsis">You can't see me</h2></header> <ul class="w3-ul w3-hoverable w3-pale-yellow" id="subfolderlist"> @@ -83,10 +91,7 @@ </div> </div> <div class="w3-container"> - <div id="progressbar" class="w3-progress-container"> - <div id="bufferbar" class="w3-progressbar" style="background-color:#AAA; width:0%"></div> - <div id="timebar" class="w3-progressbar w3-blue" style="width:0%"></div> - </div> + <pcm-progress id="progress-bar"></pcm-progress> </div> <div class="w3-container w3-center" style="padding:6px 0px;"> <button class="w3-btn w3-tiny" id="btn-prev"><<</button> @@ -98,6 +103,7 @@ </div> </div> +<script src="/libs/music/player.d/progress-bar.js"></script> <script src="/libs/music/player.d/pcm.js"></script> <script> function w3_open() { 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 <toblumia@outlook.com> +; // 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("<a>").attr('index', i).append( H("<li>").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<Player.audio.buffered.length; ++i) + for (var i=0; i<Player.audio.buffered.length; ++i) r = r<Player.audio.buffered.end(i) ? Player.audio.buffered.end(i) : r; - H("bufferbar").css("width", r / Player.audio.duration*100+"%"); + H("progress-bar").attr("buffer", r / Player.audio.duration*100); }; this.audio.onpause = function() { @@ -296,7 +374,7 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; that.updateMetadata(); } - H("progressbar").click(function(e) { + H("progress-bar").click(function(e) { var sr=this.getBoundingClientRect(); var p=(e.clientX-sr.left)/sr.width; that.audio.currentTime=that.audio.duration*p; @@ -306,7 +384,7 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; for(var i = 0; i < nodeList.length; i++) { var el = nodeList[i]; el.onclick = function() { - if(that.data[that.currentIndex]) H(that.nowPlaying).innerHTML(decodeURIComponent(that.data[that.currentIndex].fileName)); + if(that.data[that.currentIndex]) H(that.nowPlaying).innerHTML(displayName(that.data[that.currentIndex])); }; } @@ -346,7 +424,7 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; that.applyLoop(); setCookie("pcm-loop", that.loop, 157680000); }); - + H("btn-order").click(function() { that.order = 1 - that.order; that.applyOrder(); @@ -359,7 +437,7 @@ const AUDIO_URL='//filestorage.chrisoft.org/music/ogg/'; } } }; - + Player.init(); Player.ready(); }()); diff --git a/libs/music/player.d/progress-bar.js b/libs/music/player.d/progress-bar.js new file mode 100644 index 0000000..c5850c2 --- /dev/null +++ b/libs/music/player.d/progress-bar.js @@ -0,0 +1,97 @@ +; // SPDX-FileCopyrightText: 2021 Gary Wang <toblumia@outlook.com> +; // 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); |