UNPKG

globular-mvc

Version:

Generic template to create web-application that made use of globular as backend and materialize as css (wrap in web-component's)

746 lines (606 loc) 22.7 kB
import parser from 'iptv-playlist-parser' import { GetAudioByIdRequest, GetVideoByIdRequest } from "globular-web-client/title/title_pb.js"; import { Application } from "../Application"; import { fireResize, formatBoolean } from "./utility.js"; import { secondsToTime } from "./Audio.js"; import { generatePeerToken, getUrl, Model } from '../Model'; import { SetVideoConversionRequest } from "globular-web-client/file/file_pb.js"; import { File } from "../File"; let __videos__ = {} let __audios__ = {} export function setVideo(video) { __videos__[video.getId()] = video } export function setAudio(audio) { __audios__[audio.getId()] = audio } function replaceURLs(inputString, newURL) { const urlRegex = /(https?:\/\/[^\s]+)/g; return inputString.replace(urlRegex, newURL); } // Return the list of urls from a given m3u file. function parseM3U(m3uContent) { const lines = m3uContent.split('\n'); const urls = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Skip empty lines and comments if (line.length === 0 || line.startsWith('#')) { continue; } // Extract URLs if (line.startsWith('http')) { urls.push(line); } } return urls; } // retreive video with a given id. function getVideoInfo(globule, id, callback) { if (__videos__[id]) { callback(__videos__[id]) return } generatePeerToken(globule, token => { let rqst = new GetVideoByIdRequest rqst.setIndexpath(globule.config.DataPath + "/search/videos") rqst.setVidoeid(id) globule.titleService.getVideoById(rqst, { application: Application.application, domain: globule.domain, token: token }) .then(rsp => { let video = rsp.getVideo() video.globule = globule callback(video, token) }) .catch(err => { callback(null, null) }) }) } function getAudioInfo(globule, id, callback) { if (__audios__[id]) { callback(__audios__[id]) return } generatePeerToken(globule, token => { let rqst = new GetAudioByIdRequest rqst.setIndexpath(globule.config.DataPath + "/search/audios") rqst.setAudioid(id) globule.titleService.getAudioById(rqst, { application: Application.application, domain: globule.domain, token: token }) .then(rsp => { let audio = rsp.getAudio() audio.globule = globule callback(audio, token) }) .catch(err => { callback(null, null) }) }) } function shuffleArray(array) { for (var i = array.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = array[i]; array[i] = array[j]; array[j] = temp; } return array; } /** * A play list is accociated with a directory. So you must specify the path * where media files can be read... */ export class PlayList extends HTMLElement { // attributes. // Create the applicaiton view. constructor(dir) { super() // Set the shadow dom. this.attachShadow({ mode: 'open' }); this.index = 0; // Innitialisation of the layout. this.shadowRoot.innerHTML = ` <style> ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { background: var(--palette-background-default); } ::-webkit-scrollbar-thumb { background: var(--palette-divider); } #container { overflow-y: auto; overflow-x: hidden; background-color: black; height: 100%; } #items{ display: table; border-collapse: separate; flex-flow: 1; padding-bottom: 50px; max-width: 100vw; } ::slotted(globular-playlist-item) { display: table-row; padding: 2px; } ::slotted(.playing) { -webkit-box-shadow: inset 5px 5px 15px 5px #152635; box-shadow: inset 5px 5px 15px 5px #152635; } </style> <div id="container"> <div id="items"> <slot></slot> </div> </div> ` // give the focus to the input. this.style.display = "table" this.playlist = null; this.audioPlayer = null; this.videoPlayer = null; this.items = [] } // The connection callback. connectedCallback() { fireResize() } clear() { this.items = [] this.index = 0 } play(item, restart, resume) { this.index = this.items.indexOf(item); let url = new URL(item.url) let globule = Model.getGlobule(url.hostname) getVideoInfo(globule, item.id, (video, token) => { let url = decodeURIComponent(item.url) url = url.split("?")[0] url += "?application=" + Model.application if (video) { if (restart) localStorage.removeItem(video.getId()) // play the video at start... if (resume) this.videoPlayer.play(url, globule, video) } else { getAudioInfo(globule, item.id, (audio) => { if (audio) if (File.hasLocal) { // Get the file path part from the url and test if a local copy exist, if so I will use it. File.hasLocal(path, exists => { if (exists) { var parser = document.createElement('a'); parser.href = url this.audioPlayer.play(decodeURIComponent(parser.pathname), globule, audio, true) } else { this.audioPlayer.play(url, globule, audio) } }) } else { this.audioPlayer.play(url, globule, audio) } }) } }) } playNext() { if (this.index < this.items.length - 1) { this.index++ this.setPlaying(this.items[this.index], true, true) } else { this.index = 0 this.items.forEach(item => { item.stopPlaying() item.classList.remove("playing") }) let loop = false if (this.audioPlayer) { loop = this.audioPlayer.loop } else if (this.videoPlayer) { loop = this.videoPlayer.loop } if (loop) { this.setPlaying(this.items[this.index], true, true) } } } stop() { this.index = 0 this.items.forEach(item => { item.stopPlaying() item.classList.remove("playing") }) let item = this.items[this.index] // set the scoll position... item.scrollIntoView({ behavior: 'smooth' }) } playPrevious() { if (this.index > 0) { this.index-- this.setPlaying(this.items[this.index], true, true) } } load(txt, globule, player, callback) { // keep refrence to the audio player. if (player.constructor.name == "Audio_AudioPlayer") { this.audioPlayer = player; } else if (player.constructor.name == "Video_VideoPlayer") { this.videoPlayer = player; } this.itmes = [] generatePeerToken(globule, token => { // if a playlist is given directly... if (txt.startsWith("#EXTM3U")) { console.log("Parsing playlist...", txt) // remove caracter not digest by the parser... const urls = parseM3U(txt); // replace the urls to please the parser... txt = replaceURLs(txt, "http://localhost:8080/") const result = parser.parse(txt) // set back the original urls... result.items.forEach((item, index) => { item.url = urls[index] }) this.playlist = result; this.refresh(callback) fireResize() } else { let url = getUrl(globule) // url += txt //url += path txt.split("/").forEach(item => { item = item.trim() if (item.length > 0) { url += "/" + encodeURIComponent(item) } }) url += "?application=" + Model.application if (token) { url += "&token=" + token } // Fetch the playlist file, using xhr for example var xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.overrideMimeType("audio/x-mpegurl"); // Needed, see below. xhr.onload = (evt) => { // remove caracter not digest by the parser... let txt = evt.target.response const urls = parseM3U(txt); // replace the urls to please the parser... txt = replaceURLs(txt, "http://localhost:8080/") const result = parser.parse(txt) // set back the original urls... result.items.forEach((item, index) => { item.url = urls[index] }) this.playlist = result; this.refresh(callback) fireResize() }; xhr.send(); } }) } refresh(callback) { // clear the content... this.innerHTML = "" let newPlayListItem = (index) => { let item = this.playlist.items[index] if (item) { new PlayListItem(item, this, index, (item_) => { item_.onmouseover = () => { if (!item_.isPlaying) { item_.hidePauseButton() item_.showPlayButton() } } item_.onmouseleave = () => { if (!item_.isPlaying) { item_.hidePlayButton() item_.hidePauseButton() } } this.items.push(item_) this.appendChild(item_) if (this.playlist.items.length == this.items.length) { this.orderItems() // play the first item... if (this.items.length > 0) { this.setPlaying(this.items[0], true, true) } callback() } else { newPlayListItem(this.items.length) } }) } } // start recursion newPlayListItem(this.items.length) } setPlaying(item, restart, resume) { this.items.forEach(item => { item.stopPlaying() item.classList.remove("playing") }) this.index = this.items.indexOf(item); item.setPlaying() this.play(item, restart, resume) item.classList.add("playing") if (this.audioPlayer != null) { this.audioPlayer.setTarckInfo(this.index, this.items.length) } else if (this.videoPlayer != null) { this.videoPlayer.setTarckInfo(this.index, this.items.length) } // set the scoll position... item.scrollIntoView({ behavior: 'smooth' }) } pausePlaying() { let item = this.items[this.index] if (item) item.pausePlaying() } resumePlaying() { let item = this.items[this.index] if (item) this.setPlaying(item, false, false) } orderItems() { // sort by items index... this.innerHTML = "" let suffle = false; if (this.audioPlayer) { suffle = this.audioPlayer.shuffle } else if (this.videoPlayer) { suffle = this.videoPlayer.shuffle } if (suffle) { this.items = shuffleArray(this.items) } else { this.items = this.items.sort((a, b) => { return a.index - b.index }) } this.items.forEach(item => this.appendChild(item)) } count() { return this.items.length } } customElements.define('globular-playlist', PlayList) /** * Sample empty component */ export class PlayListItem extends HTMLElement { // attributes. // Create the applicaiton view. constructor(item, parent, index, callback) { super() // Set the shadow dom. this.attachShadow({ mode: 'open' }); this.item = item; this.parent = parent; this.index = index; this.audio = null; this.video = null; // Innitialisation of the layout. this.shadowRoot.innerHTML = ` <style> #container img{ height: 48px; } .title{ font-size: 1rem; color: white; max-width: 400px; } :host-context(globular-playlist) { display: table; width: 100%; } .cell img { height: 48px; border-left: 1px solid black; border-bottom: 1px solid black; border-right: 1px solid #424242; border-top: 1px solid #424242; } .cell { display: table-cell; vertical-align: middle; padding: 10px 5px 10px 5px; color: white; } iron-icon:hover { cursor: pointer; } </style> <div class="cell"> <iron-icon id="play-arrow" style="--iron-icon-fill-color: white;" title="Play" style="visibility: hidden;" icon="av:play-arrow"></iron-icon> <iron-icon id="pause" style="--iron-icon-fill-color: white;" title="Pause" style="visibility: hidden; display: none;" icon="av:pause"></iron-icon> </div> <div class="cell"> <img id="title-image"></img> </div> <div class="cell"> <div style="display: flex; flex-direction: column; padding-left: 10px; padding-rigth: 10px;"> <div id="title-div" class="title"></div> <div style="font-size: .85rem; display: flex;"> <span id="title-artist-span" style="flex-grow: 1; max-width: 400px; min-width: 160px;" class="author"></span> <span id="title-duration-span"> </span> </div> </div> </div> ` // give the focus to the input. this.playBtn = this.shadowRoot.querySelector("#play-arrow") this.pauseBtn = this.shadowRoot.querySelector("#pause") this.titleDuration = this.shadowRoot.querySelector("#title-duration-span") this.needResume = false this.playBtn.onclick = () => { if (this.needResume) { this.parent.setPlaying(this, false, true) } else { this.parent.setPlaying(this, true, true) } this.hidePlayButton() this.showPauseButton() } this.pauseBtn.onclick = () => { this.hidePauseButton() this.showPlayButton() if (this.parent.audioPlayer) { this.parent.audioPlayer.pause() } else if (this.parent.videoPlayer) { this.parent.videoPlayer.stop() this.needResume = true } } this.isPlaying = false; this.id = item.tvg.id; this.url = item.url this.src = item.tvg.url let url = new URL(item.url) this.globule = Model.getGlobule(url.hostname) // init the uderlying info... getAudioInfo(this.globule, this.id, audio => { this.audio = audio; if (audio == null) { getVideoInfo(this.globule, this.id, video => { if (video != null) { this.video = video this.shadowRoot.querySelector("#title-div").innerHTML = video.getDescription() this.shadowRoot.querySelector("#title-artist-span").innerHTML = video.getPublisherid().getName() this.shadowRoot.querySelector("#title-image").src = video.getPoster().getContenturl() if (this.video.getDuration()) this.titleDuration.innerHTML = this.parseDuration(this.video.getDuration()) callback(this) } }) } else { this.shadowRoot.querySelector("#title-div").innerHTML = audio.getTitle() this.shadowRoot.querySelector("#title-artist-span").innerHTML = audio.getArtist() this.shadowRoot.querySelector("#title-image").src = audio.getPoster().getContenturl() if (this.audio.getDuration()) this.titleDuration.innerHTML = this.parseDuration(this.audio.getDuration()) callback(this) } }) } // load item if not already loaded and get it image url... getImage(callback) { if (this.audio) { callback(this.audio.getPoster().getContenturl()) return } if (this.video) { callback(this.video.getPoster().getContenturl()) return } getAudioInfo(this.globule, this.id, audio => { if (audio == null) { getVideoInfo(this.globule, this.id, video => { if (video != null) { this.video = video callback(this.video.getPoster().getContenturl()) } }) } else { this.audio = audio callback(this.audio.getPoster().getContenturl()) } }) } // extract the duration info from the raw data. parseDuration(duration) { // display the track lenght... let obj = secondsToTime(duration) var hours = obj.h var min = obj.m var sec = obj.s let hours_ = (hours < 10) ? '0' + hours : hours; let minutes_ = (min < 10) ? '0' + min : min; let seconds_ = (sec < 10) ? '0' + sec : sec; if (hours > 0) return hours_ + ":" + minutes_ + ":" + seconds_; if (min > 0) return minutes_ + ":" + seconds_; return seconds_ + "'s"; } // Parse the name and split the information from it... parseName(name) { name = name.replace("|", " - ").replace("Official Music Video", "") // make use of - instead of | as separator. if (name.indexOf(" - ") == -1) { return { title: name, author: "", featuring: "" } } // Try the best to get correct values... let title_ = name.split(" - ")[1].replace(/FEAT./i, "ft."); let feat = "" if (title_.indexOf(" ft.") != -1) { feat = title_.split(" ft.")[1] title_ = title_.split(" ft.")[0] } else if (title_.indexOf("(ft.") != -1) { feat = title_.split("(ft.")[1].replace(")", 0) title_ = title_.split(" ft.")[0] } title_ = title_.replace(/ *\([^)]*\) */g, " ").replace(/ *\[[^)]*\] */g, " ").replace(/LYRICS/i, ""); let author = name.split(" - ")[0].replace(/FEAT./i, "ft.").trim() if (author.indexOf(" ft.") != -1) { feat = author.split(" ft.")[1] author = author.split(" ft.")[0] } else if (author.indexOf("(ft.") != -1) { feat = author.split("(ft.")[1].replace(")", 0) author = author.split(" ft.")[0] } feat = feat.replace(/ *\([^)]*\) */g, " ").replace(/ *\[[^)]*\] */g, " "); author = author.replace(/ *\([^)]*\) */g, " ").replace(/ *\[[^)]*\] */g, " "); return { title: title_, author: author, featuring: feat } } setPlaying() { this.playBtn.style.visibility = "hidden" this.pauseBtn.style.visibility = "visible" this.hidePlayButton() this.showPauseButton() this.isPlaying = true; } pausePlaying() { this.playBtn.style.visibility = "visible" this.pauseBtn.style.visibility = "hidden" this.hidePauseButton() this.showPlayButton() this.isPlaying = false; } stopPlaying() { this.playBtn.style.visibility = "hidden" this.pauseBtn.style.visibility = "hidden" this.pauseBtn.style.display = "none" this.playBtn.style.display = "block" this.isPlaying = false; this.needResume = false; } showPlayButton() { this.pauseBtn.style.display = "none" this.playBtn.style.display = "block" this.pauseBtn.style.visibility = "hidden" this.playBtn.style.visibility = "visible" } hidePlayButton() { //this.playBtn.style.display = "none" this.playBtn.style.visibility = "hidden" } hidePauseButton() { this.pauseBtn.style.display = "none" this.pauseBtn.style.visibility = "hidden" } showPauseButton() { this.pauseBtn.style.display = "block" this.playBtn.style.display = "none" this.pauseBtn.style.visibility = "visible" this.playBtn.style.visibility = "hidden" } } customElements.define('globular-playlist-item', PlayListItem)