summaryrefslogtreecommitdiff
path: root/libs/music
diff options
context:
space:
mode:
Diffstat (limited to 'libs/music')
-rw-r--r--libs/music/pcm18
-rwxr-xr-xlibs/music/player.d/cgi-bin/pcm.cgi76
-rw-r--r--libs/music/player.d/pcm.js200
-rw-r--r--libs/music/player.d/progress-bar.js97
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">&#9776;</span>&nbsp;Pineapple Cloud Music</h2>
+ <h2 class="ellipsis"><span class="w3-opennav w3-xlarge w3-left w3-hide-large" onclick="w3_open()" id="openNav">&#9776;</span>&nbsp;<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">&lt;&lt;</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);