diff --git a/README.md b/README.md index 2a9a51fb..88f5409c 100644 --- a/README.md +++ b/README.md @@ -29,20 +29,22 @@ IDK, it would be really nice of you to contribute, check the poorly written [CON - [x] Audio player - [x] Accounts and Profiles - [x] Playlists -- [ ] Share playlists +- [x] Share playlists - [ ] Vote songs in playlists -- [ ] Write a better YouTube scraper (or try to fix the quota thing) +- [x] Songs queue - [ ] Refactor the code (never gonna happen) ## Technologies used - **[Go](https://golang.org)**: Main programming language. +- **[JavaScript](https://developer.mozilla.org/en-US/docs/Web/javascript)**: Dynamic client logic. +- **[Python](https://python.org)**: Used for the YouTube download service. - **[templ](https://templ.guide)**: The better [html/template](https://pkg.go.dev/html/template). - **[htmx](https://htmx.org)**: The front-end library of peace. - **[GORM](https://gorm.io)**: The fantastic ORM library for Golang. - **[MariaDB](https://mariadb.org)**: Relational database. -- **[Python](https://python.org)**: Used for the YouTube download service. - **[yt-dlp](https://github.com/yt-dlp/yt-dlp)**: YouTube download helper. +- **[pytube](https://github.com/pytube/pytube)**: YouTube download helper. - **[minify](https://github.com/tdewolff/minify)**: Minify static text files. ## Run locally diff --git a/services/youtube/search/search_scraper.go b/services/youtube/search/search_scraper.go index 12baeeb7..bbf8dfe1 100644 --- a/services/youtube/search/search_scraper.go +++ b/services/youtube/search/search_scraper.go @@ -52,11 +52,15 @@ func (y *ScraperSearch) Search(query string) (results []entities.Song, err error duration[0] = "0" + duration[0] } if len(duration) == 3 { - durationNum, err := strconv.Atoi(duration[0]) + hoursNum, err := strconv.Atoi(duration[0]) if err != nil { continue } - if durationNum > 2 { + minsNum, err := strconv.Atoi(duration[1]) + if err != nil { + continue + } + if hoursNum >= 1 && minsNum > 30 { continue } } diff --git a/static/icons/add-to-queue.svg b/static/icons/add-to-queue.svg new file mode 100644 index 00000000..e26e8c73 --- /dev/null +++ b/static/icons/add-to-queue.svg @@ -0,0 +1,3 @@ + diff --git a/static/js/player.js b/static/js/player.js index 551350e3..c264b807 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -1,45 +1,33 @@ "use strict"; -const loopModes = [ - { icon: "loop-off-icon.svg", mode: "OFF" }, - { icon: "loop-once-icon.svg", mode: "ONCE" }, - { icon: "loop-icon.svg", mode: "ALL" }, -]; - +// collapsed player's elements const playPauseToggleEl = document.getElementById("play"), - playPauseToggleExapndedEl = document.getElementById("play-expand"), shuffleEl = document.getElementById("shuffle"), nextEl = document.getElementById("next"), prevEl = document.getElementById("prev"), loopEl = document.getElementById("loop"), songNameEl = document.getElementById("song-name"), - songNameExpandedEl = document.getElementById("song-name-expanded"), artistNameEl = document.getElementById("artist-name"), - artistNameExpandedEl = document.getElementById("artist-name-expanded"), songSeekBarEl = document.getElementById("song-seek-bar"), - songSeekBarExpandedEl = document.getElementById("song-seek-bar-expanded"), songDurationEl = document.getElementById("song-duration"), - songDurationExpandedEl = document.getElementById("song-duration-expanded"), songCurrentTimeEl = document.getElementById("song-current-time"), - songCurrentTimeExpandedEl = document.getElementById( - "song-current-time-expanded", - ), songImageEl = document.getElementById("song-image"), - songImageExpandedEl = document.getElementById("song-image-expanded"), audioPlayerEl = document.getElementById("audio-player"), muzikkContainerEl = document.getElementById("muzikk"), - zePlayerEl = document.getElementById("ze-player"), - zeCollapsedMobilePlayer = document.getElementById( - "ze-collapsed-mobile-player", - ), - zeExpandedMobilePlayer = document.getElementById("ze-expanded-mobile-player"); + playerEl = document.getElementById("ze-player"), + collapsedMobilePlayer = document.getElementById("ze-collapsed-mobile-player"); -let shuffleSongs = false; -let currentLoopIdx = 0; -/** - * @type{PlayerUI} - */ -let ui; +// expanded player's elements +const playPauseToggleExapndedEl = document.getElementById("play-expand"), + songNameExpandedEl = document.getElementById("song-name-expanded"), + artistNameExpandedEl = document.getElementById("artist-name-expanded"), + songSeekBarExpandedEl = document.getElementById("song-seek-bar-expanded"), + songDurationExpandedEl = document.getElementById("song-duration-expanded"), + songCurrentTimeExpandedEl = document.getElementById( + "song-current-time-expanded", + ), + songImageExpandedEl = document.getElementById("song-image-expanded"), + expandedMobilePlayer = document.getElementById("ze-expanded-mobile-player"); /** * @typedef {object} Song @@ -52,86 +40,404 @@ let ui; * @property {string} added_at */ -class PlayerUI { - show() { - muzikkContainerEl.style.display = "block"; - } +/** + * @typedef {object} Playlist + * @property {string} public_id + * @property {string} title + * @property {string} songs_count + * @property {Song[]} songs + */ - hide() { - muzikkContainerEl.style.display = "none"; - } +/** + * @typedef {object} PlayerState + * @property {LoopMode} loopMode + * @property {boolean} shuffled + * @property {Playlist} playlist + * @property {number} currentSongIdx + */ - expand() { - if (!zePlayerEl.classList.contains("exapnded")) { - zePlayerEl.classList.add("exapnded"); - zeCollapsedMobilePlayer.classList.add("hidden"); - zeExpandedMobilePlayer.classList.remove("hidden"); - } - } +/** + * @enum {LoopMode} + */ +const LOOP_MODES = Object.freeze({ + ALL: "ALL", + OFF: "OFF", + ONCE: "ONCE", +}); - collapse() { - if (zePlayerEl.classList.contains("exapnded")) { - zePlayerEl.classList.remove("exapnded"); - zeExpandedMobilePlayer.classList.add("hidden"); - zeCollapsedMobilePlayer.classList.remove("hidden"); - } - } +/** + * @type{PlayerState} + */ +const playerState = { + loopMode: LOOP_MODES.OFF, + shuffled: false, + currentSongIdx: 0, + playlist: { + title: "Queue", + songs_count: 0, + public_id: "", + songs: [], + }, +}; + +function isSingleSong() { + return playerState.playlist.songs.length <= 1; +} - /** - * @param {boolean} isPlay - */ - setPlay(isPlay = false) { - if (isPlay) { - playPauseToggleEl.innerHTML = Player.icons.pause; - if (!!playPauseToggleExapndedEl) { - playPauseToggleExapndedEl.innerHTML = Player.icons.pause; - } +/** + * @returns {[Function, Function]} + */ +function looper() { + const loopModes = [LOOP_MODES.OFF, LOOP_MODES.ONCE, LOOP_MODES.ALL]; + let currentLoopIdx = 0; + + const __toggle = () => { + if (isSingleSong()) { + currentLoopIdx = currentLoopIdx === 0 ? 1 : 0; } else { - playPauseToggleEl.innerHTML = Player.icons.play; - if (!!playPauseToggleExapndedEl) { - playPauseToggleExapndedEl.innerHTML = Player.icons.play; - } + currentLoopIdx = (currentLoopIdx + 1) % loopModes.length; } - } + loopEl.innerHTML = + Player.icons[ + loopModes[currentLoopIdx] === LOOP_MODES.ALL + ? "loop" + : loopModes[currentLoopIdx] === LOOP_MODES.ONCE + ? "loopOnce" + : loopModes[currentLoopIdx] === LOOP_MODES.OFF + ? "loopOff" + : "loopOff" + ]; + }; + + const __handle = () => { + switch (loopModes[currentLoopIdx]) { + case LOOP_MODES.OFF: + stopMuzikk(); + if (!isSingleSong()) { + nextMuzikk(); + } + break; + case LOOP_MODES.ONCE: + stopMuzikk(); + playMuzikk(); + break; + case LOOP_MODES.ALL: + if (!isSingleSong()) { + nextMuzikk(); + } + break; + } + }; /** - * @param {boolean} isShuffle + * @param {LoopMode} loopMode + * @returns {boolean} */ - setShuffle(isShuffle = false) { - if (isShuffle) { - shuffleEl.innerHTML = Player.icons.shuffleOff; + const __check = (loopMode) => loopMode === loopModes[currentLoopIdx]; + + return [__toggle, __handle, __check]; +} +/** + * @param {HTMLElement} el + * @param {string} icon + */ +const setPlayerButtonIcon = (el, icon) => { + if (!!el && !!icon) { + el.innerHTML = icon; + } +}; + +/** + * @param {boolean} loading + * @param {string} fallback is used when loading is false, that is to reset + * the loading thingy + */ +function setLoading(loading, fallback) { + if (loading) { + setPlayerButtonIcon(playPauseToggleEl, Player.icons.loading); + setPlayerButtonIcon(playPauseToggleExapndedEl, Player.icons.loading); + document.body.style.cursor = "progress"; + return; + } + if (fallback) { + setPlayerButtonIcon(playPauseToggleEl, fallback); + setPlayerButtonIcon(playPauseToggleExapndedEl, fallback); + } + document.body.style.cursor = "auto"; +} + +/** + * @param {HTMLAudioElement} audioEl + * + * @returns {[Function, Function, Function]} + */ +function playPauser(audioEl) { + const __play = () => { + audioEl.play(); + const songEl = document.getElementById( + "song-" + playerState.playlist.songs[playerState.currentSongIdx].yt_id, + ); + if (!!songEl) { + songEl.style.backgroundColor = "var(--accent-color-30)"; + } + setPlayerButtonIcon(playPauseToggleEl, Player.icons.pause); + setPlayerButtonIcon(playPauseToggleExapndedEl, Player.icons.pause); + }; + const __pause = () => { + audioEl.pause(); + setPlayerButtonIcon(playPauseToggleEl, Player.icons.play); + setPlayerButtonIcon(playPauseToggleExapndedEl, Player.icons.play); + }; + const __toggle = () => { + if (audioEl.paused) { + __play(); } else { - shuffleEl.innerHTML = Player.icons.shuffle; + __pause(); } - } + }; + return [__play, __pause, __toggle]; +} + +/** + * @param {HTMLAudioElement} audioEl + * + * @returns {Function} + */ +function stopper(audioEl) { + return () => { + audioEl.pause(); + audioEl.currentTime = 0; + const songEl = document.getElementById( + "song-" + playerState.playlist.songs[playerState.currentSongIdx].yt_id, + ); + if (!!songEl) { + songEl.style.backgroundColor = "var(--secondary-color-20)"; + } + setPlayerButtonIcon(playPauseToggleEl, Player.icons.play); + setPlayerButtonIcon(playPauseToggleExapndedEl, Player.icons.play); + }; +} + +/** + * @param {PlayerState} state + * + * @returns {Function} + */ +function shuffler(state) { + return () => { + state.shuffled = !state.shuffled; + setPlayerButtonIcon( + shuffleEl, + state.shuffled ? Player.icons.shuffle : Player.icons.shuffleOff, + ); + }; +} +/** + * @param {PlayerState} state + * + * @returns {[Function, Function, Function, Function]} + */ +function playlister(state) { /** - * @param {boolean} loading - * @param {string} fallback is used when loading is false, that is to reset - * the loading thingy + * @param {string} songYtId + * @param {Playlist} playlist */ - setLoading(loading, fallback) { - if (loading) { - playPauseToggleEl.innerHTML = Player.icons.loading; - if (!!playPauseToggleExapndedEl) { - playPauseToggleExapndedEl.innerHTML = Player.icons.loading; + const __setSongInPlaylistStyle = (songYtId, playlist) => { + for (const _song of playlist.songs) { + const songEl = document.getElementById("song-" + _song.yt_id); + if (!songEl) { + continue; + } + if (songYtId === _song.yt_id) { + songEl.style.backgroundColor = "var(--accent-color-30)"; + songEl.scrollIntoView(); + } else { + songEl.style.backgroundColor = "var(--secondary-color-20)"; } - document.body.style.cursor = "progress"; + } + }; + + const __updateSongPlays = async () => { + if (!state.playlist.public_id) { return; } - if (fallback) { - playPauseToggleEl.innerHTML = fallback; - if (!!playPauseToggleExapndedEl) { - playPauseToggleExapndedEl.innerHTML = fallback; - } + await fetch( + "/api/increment-song-plays?" + + new URLSearchParams({ + "song-id": state.playlist.songs[state.currentSongIdx].yt_id, + "playlist-id": state.playlist.public_id, + }).toString(), + { + method: "PUT", + }, + ).catch((err) => console.error(err)); + }; + + const __next = () => { + if (checkLoop(LOOP_MODES.ONCE)) { + stopMuzikk(); + playMuzikk(); + return; + } + if ( + !checkLoop(LOOP_MODES.ALL) && + !state.shuffled && + state.currentSongIdx + 1 >= state.playlist.songs.length + ) { + stopMuzikk(); + return; } - document.body.style.cursor = "auto"; + state.currentSongIdx = state.shuffled + ? Math.floor(Math.random() * state.playlist.songs.length) + : checkLoop(LOOP_MODES.ALL) && + state.currentSongIdx + 1 >= state.playlist.songs.length + ? 0 + : state.currentSongIdx + 1; + const songToPlay = state.playlist.songs[state.currentSongIdx]; + playSongFromPlaylist(songToPlay.yt_id, state.playlist); + __updateSongPlays(); + __setSongInPlaylistStyle(songToPlay.yt_id, state.playlist); + }; + + const __prev = () => { + if (checkLoop(LOOP_MODES.ONCE)) { + stopMuzikk(); + playMuzikk(); + return; + } + if ( + !checkLoop(LOOP_MODES.ALL) && + !state.shuffled && + state.currentSongIdx - 1 < 0 + ) { + stopMuzikk(); + return; + } + state.currentSongIdx = state.shuffled + ? Math.floor(Math.random() * state.playlist.songs.length) + : checkLoop(LOOP_MODES.ALL) && state.currentSongIdx - 1 < 0 + ? state.playlist.songs.length - 1 + : state.currentSongIdx - 1; + const songToPlay = state.playlist.songs[state.currentSongIdx]; + playSongFromPlaylist(songToPlay.yt_id, state.playlist); + __updateSongPlays(); + __setSongInPlaylistStyle(songToPlay.yt_id, state.playlist); + }; + + const __remove = (songYtId, playlistId) => { + const songIndex = state.playlist.songs.findIndex( + (song) => song.yt_id === songYtId, + ); + if (songIndex >= 0) { + state.playlist.songs.splice(songIndex, 1); + } + + Utils.showLoading(); + fetch( + "/api/toggle-song-in-playlist?song-id=" + + songYtId + + "&playlist-id=" + + playlistId + + "&remove=true", + { + method: "PUT", + }, + ) + .then((res) => { + if (res.ok) { + const songEl = document.getElementById("song-" + songYtId); + if (!!songEl) { + songEl.remove(); + } + } else { + window.alert("Oopsie something went wrong!"); + } + }) + .catch((err) => { + window.alert("Oopsie something went wrong!\n", err); + }) + .finally(() => { + Utils.hideLoading(); + }); + }; + + return [__next, __prev, __remove, __setSongInPlaylistStyle]; +} + +/** + * @param {string} songYtId + */ +async function downloadSong(songYtId) { + return await fetch("/api/song?id=" + songYtId).catch((err) => + console.error(err), + ); +} + +/** + * @param {string} songYtId + * @param {string} songTitle + */ +async function downloadSongToDevice(songYtId, songTitle) { + Utils.showLoading(); + await downloadSong(songYtId) + .then(() => { + const a = document.createElement("a"); + a.href = `/muzikkx/${songYtId}.mp3`; + a.download = `${songTitle}.mp3`; + a.click(); + }) + .finally(() => { + Utils.hideLoading(); + }); +} + +/** + * @param {string} songYtId + */ +async function downloadToApp() { + throw new Error("not implemented!"); +} + +function show() { + muzikkContainerEl.style.display = "block"; +} + +function hide() { + muzikkContainerEl.style.display = "none"; +} + +function expand() { + if (!playerEl.classList.contains("exapnded")) { + playerEl.classList.add("exapnded"); + collapsedMobilePlayer.classList.add("hidden"); + expandedMobilePlayer.classList.remove("hidden"); } +} - /** - * @param {Song} song - */ - setDetails(song) { +function collapse() { + if (playerEl.classList.contains("exapnded")) { + playerEl.classList.remove("exapnded"); + expandedMobilePlayer.classList.add("hidden"); + collapsedMobilePlayer.classList.remove("hidden"); + } +} + +/** + * @param {Song} song + */ +async function playSong(song) { + setLoading(true); + show(); + + await downloadSong(song.yt_id).then(() => { + stopMuzikk(); + audioPlayerEl.src = `/muzikkx/${song.yt_id}.mp3`; + audioPlayerEl.load(); + }); + + // song's details setting, yada yada + { if (song.title) { songNameEl.innerHTML = song.title; songNameEl.title = song.title; @@ -170,68 +476,68 @@ class PlayerUI { songImageExpandedEl.innerHTML = ""; } } + setMediaSessionMetadata(song); + playMuzikk(); +} - /** - * @param {number} time - */ - setCurrentTime(time) { - const currentTime = Math.floor(time); - if (songCurrentTimeEl) { - songCurrentTimeEl.innerHTML = Utils.formatTime(currentTime); - } - if (songCurrentTimeExpandedEl) { - songCurrentTimeExpandedEl.innerHTML = Utils.formatTime(currentTime); - } - if (songSeekBarEl) { - songSeekBarEl.value = Math.ceil(currentTime); - } - if (songSeekBarExpandedEl) { - songSeekBarExpandedEl.value = Math.ceil(currentTime); - } - } +/** + * @param {Song} song + */ +function playSingleSong(song) { + playerState.playlist = { + title: "Queue", + songs_count: 1, + public_id: "", + songs: [song], + }; + playerState.currentSongIdx = 0; + playSong(song); +} - /** - * @param {number} duration - */ - setDuration(duration) { - if (isNaN(duration)) { - duration = 0; - } - songSeekBarEl.max = Math.ceil(duration); - songSeekBarEl.value = 0; - if (songSeekBarExpandedEl) { - songSeekBarExpandedEl.max = Math.ceil(duration); - songSeekBarExpandedEl.value = 0; - } - console.log("duration", songDurationEl); - if (songDurationEl) { - songDurationEl.innerHTML = Utils.formatTime(duration); - } - if (songDurationExpandedEl) { - songDurationExpandedEl.innerHTML = Utils.formatTime(duration); - } +/** + * @param {string} songYtId + * @param {Playlist} playlist + */ +function playSongFromPlaylist(songYtId, playlist) { + const songIdx = playlist.songs.findIndex((s) => s.yt_id === songYtId); + if (songIdx < 0) { + alert("Invalid song!"); + return; } + playerState.playlist = playlist; + playerState.currentSongIdx = songIdx; + const songToPlay = playlist.songs[songIdx]; + highlightSongInPlaylist(songToPlay.yt_id, playlist); + playSong(songToPlay); +} - loopIcon() { - switch (loopModes[currentLoopIdx].mode) { - case "OFF": - return Player.icons.loopOff; - case "ONCE": - return Player.icons.loopOnce; - case "ALL": - return Player.icons.loop; - } +/** + * @param {Song} song + */ +function appendSongToCurrentQueue(song) { + if ( + playerState.playlist.songs.findIndex((s) => s.yt_id === song.yt_id) !== -1 + ) { + alert(`${song.title} exists in the queue!`); + return; } + playerState.playlist.songs.push(song); + alert(`Added ${song.title} to the queue!`); +} + +function addSongToPlaylist() { + throw new Error("not implemented!"); } /** * @param {Song} song */ -function setMediaSession(song) { +function setMediaSessionMetadata(song) { if (!("mediaSession" in navigator)) { console.error("Browser doesn't support mediaSession"); return; } + navigator.mediaSession.metadata = new MediaMetadata({ title: song.title, artist: song.artist, @@ -251,224 +557,57 @@ function setMediaSession(song) { }; }), }); - - navigator.mediaSession.setActionHandler("play", () => { - playPauseToggle(); - }); - - navigator.mediaSession.setActionHandler("pause", () => { - playPauseToggle(); - }); - - navigator.mediaSession.setActionHandler("stop", () => { - stopMuzikk(); - }); - - navigator.mediaSession.setActionHandler("seekbackward", () => { - let seekTo = -10; - if (audioPlayerEl.currentTime + seekTo < 0) { - seekTo = 0; - } - audioPlayerEl.currentTime += seekTo; - }); - - navigator.mediaSession.setActionHandler("seekforward", () => { - let seekTo = +10; - if (audioPlayerEl.currentTime + seekTo > audioPlayerEl.duration) { - seekTo = 0; - } - audioPlayerEl.currentTime += seekTo; - }); - - navigator.mediaSession.setActionHandler("seekto", (a) => { - const seekTime = Number(a.seekTime); - audioPlayerEl.currentTime = seekTime; - }); - - navigator.mediaSession.setActionHandler("previoustrack", () => { - previousMuzikk(); - }); - - navigator.mediaSession.setActionHandler("nexttrack", () => { - nexMuzikk(); - }); - - navigator.mediaSession.setActionHandler("stop", () => { - stopMuzikk(); - }); -} - -function nexMuzikk() { - if (!Player.playlistPlayer) { - return; - } - Player.playlistPlayer.next( - shuffleSongs, - loopModes[currentLoopIdx].mode === "ALL", - ); -} - -function previousMuzikk() { - if (!Player.playlistPlayer) { - return; - } - Player.playlistPlayer.previous( - shuffleSongs, - loopModes[currentLoopIdx].mode === "ALL", - ); -} - -function playMuzikk() { - audioPlayerEl.play(); - ui.setPlay(true); -} - -function pauseMuzikk() { - audioPlayerEl.pause(); - ui.setPlay(false); -} - -function stopMuzikk() { - pauseMuzikk(); - audioPlayerEl.currentTime = 0; -} - -function playPauseToggle() { - if (audioPlayerEl.paused) { - playMuzikk(); - } else { - pauseMuzikk(); - } -} - -function toggleShuffle() { - if (!Player.playlistPlayer) { - alert("Shuffling can't be enabled for a single song!"); - return; - } - ui.setShuffle(shuffleSongs); - shuffleSongs = !shuffleSongs; -} - -/** - * @param {string} songYtId - */ -async function downloadSong(songYtId) { - return await fetch("/api/song?id=" + songYtId).catch((err) => - console.error(err), - ); -} - -/** - * @param {string} songYtId - * @param {string} songTitle - */ -async function downloadSongToDevice(songYtId, songTitle) { - Utils.showLoading(); - await downloadSong(songYtId) - .then(() => { - const a = document.createElement("a"); - a.href = `/muzikkx/${songYtId}.mp3`; - a.download = `${songTitle}.mp3`; - a.click(); - }) - .finally(() => { - Utils.hideLoading(); - }); -} - -/** - * @param {string} songYtId - */ -async function downloadToApp() { - throw new Error("not implemented!"); -} - -/** - * @param {Song} song - * @param {boolean} inPlaylist - */ -async function playSong(song, inPlaylist) { - if (!inPlaylist) { - Player.playlistPlayer = null; - nextEl.style.display = "none"; - prevEl.style.display = "none"; - shuffleEl.style.display = "none"; - if (currentLoopIdx > 1) { - currentLoopIdx = 0; - loopEl.innerHTML = ui.loopIcon(); - } - loopEl.children[0].style.height = "60px"; - loopEl.children[0].style.width = "55px"; - } else { - loopEl.children[0].style.height = "30px"; - loopEl.children[0].style.width = "30px"; - } - ui.setLoading(true); - ui.show(); - - await downloadSong(song.yt_id).then(() => { - stopMuzikk(); - audioPlayerEl.src = `/muzikkx/${song.yt_id}.mp3`; - audioPlayerEl.load(); - }); - - ui.setDetails(song); - setMediaSession(song); - playMuzikk(); } -function showPlayer() { - muzikkContainerEl.style.display = "block"; -} - -function hidePlayer() { - muzikkContainerEl.style.display = "none"; - audioPlayerEl.stopMuzikk(); -} +const [toggleLoop, handleLoop, checkLoop] = looper(); +const [playMuzikk, pauseMuzikk, togglePP] = playPauser(audioPlayerEl); +const stopMuzikk = stopper(audioPlayerEl); +const toggleShuffle = shuffler(playerState); +const [ + nextMuzikk, + previousMuzikk, + removeSongFromPlaylist, + highlightSongInPlaylist, +] = playlister(playerState); playPauseToggleEl.addEventListener("click", (event) => { event.stopImmediatePropagation(); event.preventDefault(); - playPauseToggle(); + togglePP(); }); playPauseToggleExapndedEl?.addEventListener("click", (event) => { event.stopImmediatePropagation(); event.preventDefault(); - playPauseToggle(); + togglePP(); }); -nextEl?.addEventListener("click", nexMuzikk); +nextEl?.addEventListener("click", nextMuzikk); prevEl?.addEventListener("click", previousMuzikk); shuffleEl?.addEventListener("click", toggleShuffle); loopEl?.addEventListener("click", (event) => { event.stopImmediatePropagation(); event.preventDefault(); - if (!Player.playlistPlayer) { - currentLoopIdx = currentLoopIdx === 0 ? 1 : 0; - } else { - currentLoopIdx = (currentLoopIdx + 1) % loopModes.length; - } - loopEl.innerHTML = ui.loopIcon(); -}); - -songSeekBarEl.addEventListener("change", (event) => { - event.stopImmediatePropagation(); - event.preventDefault(); - const seekTime = Number(event.target.value); - audioPlayerEl.currentTime = seekTime; + toggleLoop(); }); -songSeekBarEl.addEventListener("click", (event) => { - event.stopImmediatePropagation(); - event.preventDefault(); -}); +(() => { + const __handler = (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + const seekTime = Number(e.target.value); + audioPlayerEl.currentTime = seekTime; + }; + for (const event of ["change", "click"]) { + songSeekBarEl?.addEventListener(event, __handler); + songSeekBarExpandedEl?.addEventListener(event, __handler); + } +})(); audioPlayerEl.addEventListener("loadeddata", (event) => { playPauseToggleEl.disabled = null; - if (playPauseToggleExapndedEl) { + if (!!playPauseToggleExapndedEl) { playPauseToggleExapndedEl.disabled = null; } shuffleEl.disabled = null; @@ -476,33 +615,47 @@ audioPlayerEl.addEventListener("loadeddata", (event) => { prevEl.disabled = null; loopEl.disabled = null; - ui.setDuration(event.target.duration); - ui.setLoading(false, Player.icons.pause); + // set duration AAA + { + let duration = event.target.duration; + if (isNaN(duration)) { + duration = 0; + } + songSeekBarEl.max = Math.ceil(duration); + songSeekBarEl.value = 0; + if (!!songSeekBarExpandedEl) { + songSeekBarExpandedEl.max = Math.ceil(duration); + songSeekBarExpandedEl.value = 0; + } + if (!!songDurationEl) { + songDurationEl.innerHTML = Utils.formatTime(duration); + } + if (!!songDurationExpandedEl) { + songDurationExpandedEl.innerHTML = Utils.formatTime(duration); + } + } + + setLoading(false, Player.icons.pause); }); audioPlayerEl.addEventListener("timeupdate", (event) => { - ui.setCurrentTime(event.target.currentTime); + const currentTime = Math.floor(event.target.currentTime); + if (songCurrentTimeEl) { + songCurrentTimeEl.innerHTML = Utils.formatTime(currentTime); + } + if (songCurrentTimeExpandedEl) { + songCurrentTimeExpandedEl.innerHTML = Utils.formatTime(currentTime); + } + if (songSeekBarEl) { + songSeekBarEl.value = Math.ceil(currentTime); + } + if (songSeekBarExpandedEl) { + songSeekBarExpandedEl.value = Math.ceil(currentTime); + } }); audioPlayerEl.addEventListener("ended", () => { - switch (loopModes[currentLoopIdx].mode) { - case "OFF": - stopMuzikk(); - if (Player.playlistPlayer) { - Player.playlistPlayer.next(shuffleSongs, false); - } - break; - case "ONCE": - stopMuzikk(); - playMuzikk(); - break; - case "ALL": - if (Player.playlistPlayer) { - Player.playlistPlayer.next(shuffleSongs, true); - return; - } - break; - } + handleLoop(); }); audioPlayerEl.addEventListener("progress", () => { @@ -514,20 +667,69 @@ document ?.addEventListener("click", (event) => { event.stopImmediatePropagation(); event.preventDefault(); - ui.collapse(); + collapse(); }); -function init() { - ui = new PlayerUI(); -} +(() => { + if (!("mediaSession" in navigator)) { + console.error("Browser doesn't support mediaSession"); + return; + } + + navigator.mediaSession.setActionHandler("play", () => { + playMuzikk(); + }); + + navigator.mediaSession.setActionHandler("pause", () => { + pauseMuzikk(); + }); + + navigator.mediaSession.setActionHandler("stop", () => { + stopMuzikk(); + }); + + navigator.mediaSession.setActionHandler("seekbackward", () => { + let seekTo = -10; + if (audioPlayerEl.currentTime + seekTo < 0) { + seekTo = 0; + } + audioPlayerEl.currentTime += seekTo; + }); -init(); + navigator.mediaSession.setActionHandler("seekforward", () => { + let seekTo = +10; + if (audioPlayerEl.currentTime + seekTo > audioPlayerEl.duration) { + seekTo = 0; + } + audioPlayerEl.currentTime += seekTo; + }); + + navigator.mediaSession.setActionHandler("seekto", (a) => { + const seekTime = Number(a.seekTime); + audioPlayerEl.currentTime = seekTime; + }); + + navigator.mediaSession.setActionHandler("previoustrack", () => { + previousMuzikk(); + }); + + navigator.mediaSession.setActionHandler("nexttrack", () => { + nextMuzikk(); + }); + + navigator.mediaSession.setActionHandler("stop", () => { + stopMuzikk(); + }); +})(); window.Player = {}; window.Player.downloadSongToDevice = downloadSongToDevice; -window.Player.showPlayer = showPlayer; -window.Player.hidePlayer = hidePlayer; -window.Player.playSong = playSong; +window.Player.showPlayer = show; +window.Player.hidePlayer = hide; +window.Player.playSingleSong = playSingleSong; +window.Player.playSongFromPlaylist = playSongFromPlaylist; +window.Player.removeSongFromPlaylist = removeSongFromPlaylist; +window.Player.addSongToQueue = appendSongToCurrentQueue; window.Player.stopMuzikk = stopMuzikk; -window.Player.expand = () => ui.expand(); -window.Player.collapse = () => ui.collapse(); +window.Player.expand = () => expand(); +window.Player.collapse = () => collapse(); diff --git a/static/js/playlist_player.js b/static/js/playlist_player.js deleted file mode 100644 index e629cdf7..00000000 --- a/static/js/playlist_player.js +++ /dev/null @@ -1,170 +0,0 @@ -"use strict"; - -const shuffleEl1 = document.getElementById("shuffle"), - nextEl1 = document.getElementById("next"), - prevEl1 = document.getElementById("prev"); - -/** - * @typedef {object} Song - * @property {string} title - * @property {string} artist - * @property {string} duration - * @property {string} thumbnail_url - * @property {string} yt_id - * @property {number} play_times - * @property {string} added_at - */ - -/** - * @typedef {object} Playlist - * @property {string} public_id - * @property {string} title - * @property {string} songs_count - * @property {Song[]} songs - */ - -class PlaylistPlayer { - #playlist; - #currentSongIndex; - - /** - * @param {Playlist} playlist - */ - constructor(playlist) { - this.#playlist = playlist; - this.#currentSongIndex = 0; - } - - /** - * @param {string} songYtId - */ - play(songYtId = "") { - this.setSongNotPlayingStyle(); - this.#currentSongIndex = this.#playlist.songs.findIndex( - (song) => song.yt_id === songYtId, - ); - if (this.#currentSongIndex < 0) { - this.#currentSongIndex = 0; - } - const songToPlay = this.#playlist.songs[this.#currentSongIndex]; - Player.playSong(songToPlay, true); - nextEl1.style.display = "block"; - prevEl1.style.display = "block"; - shuffleEl1.style.display = "block"; - this.#updateSongPlays(); - this.setSongPlayingStyle(); - } - - next(shuffle = false, loop = false) { - this.setSongNotPlayingStyle(); - if ( - !loop && - !shuffle && - this.#currentSongIndex + 1 >= this.#playlist.songs.length - ) { - Player.stopMuzikk(); - return; - } - this.#currentSongIndex = shuffle - ? Math.floor(Math.random() * this.#playlist.songs.length) - : loop && this.#currentSongIndex + 1 >= this.#playlist.songs.length - ? 0 - : this.#currentSongIndex + 1; - const songToPlay = this.#playlist.songs[this.#currentSongIndex]; - Player.playSong(songToPlay, true); - this.#updateSongPlays(); - this.setSongPlayingStyle(); - } - - previous(shuffle = false, loop = false) { - this.setSongNotPlayingStyle(); - if (!loop && !shuffle && this.#currentSongIndex - 1 < 0) { - Player.stopMuzikk(); - return; - } - this.#currentSongIndex = shuffle - ? Math.floor(Math.random() * this.#playlist.songs.length) - : loop && this.#currentSongIndex - 1 < 0 - ? this.#playlist.songs.length - 1 - : this.#currentSongIndex - 1; - this.setSongNotPlayingStyle(); - const songToPlay = this.#playlist.songs[this.#currentSongIndex]; - Player.playSong(songToPlay, true); - this.#updateSongPlays(); - this.setSongPlayingStyle(); - } - - removeSong(songYtId) { - const songIndex = this.#playlist.songs.findIndex( - (song) => song.yt_id === songYtId, - ); - if (songIndex < 0) { - return; - } - this.#playlist.songs.splice(songIndex, 1); - } - - setSongPlayingStyle() { - const songEl = document.getElementById( - "song-" + this.#playlist.songs[this.#currentSongIndex].yt_id, - ); - songEl.style.backgroundColor = "var(--accent-color-30)"; - songEl.scrollIntoView(); - } - - setSongNotPlayingStyle() { - for (const song of this.#playlist.songs) { - document.getElementById("song-" + song.yt_id).style.backgroundColor = - "var(--secondary-color-20)"; - } - } - - async #updateSongPlays() { - await fetch( - "/api/increment-song-plays?" + - new URLSearchParams({ - "song-id": this.#playlist.songs[this.#currentSongIndex].yt_id, - "playlist-id": this.#playlist.public_id, - }).toString(), - { - method: "PUT", - }, - ).catch((err) => console.error(err)); - } -} - -/** - * @param {string} songYtId - */ -function removeSongFromPlaylist(songYtId) { - if (!Player.playlistPlayer) { - return; - } - Player.playlistPlayer.removeSong(songYtId); -} - -/** - * @param {Playlist} playlist - */ -function playPlaylist(playlist) { - Player.playlistPlayer = new PlaylistPlayer(playlist); - Player.playlistPlayer.play(); -} - -/** - * @param {string} songId - * @param {Playlist} playlist - */ -function playSongFromPlaylist(songId, playlist) { - Player.playlistPlayer = new PlaylistPlayer(playlist); - Player.playlistPlayer.play(songId); -} - -if (!window.Player) { - window.Player = {}; -} - -Player.playlistPlayer = null; -window.Player.playPlaylist = playPlaylist; -window.Player.playSongFromPlaylist = playSongFromPlaylist; -window.Player.removeSongFromPlaylist = removeSongFromPlaylist; diff --git a/views/components/player/player.templ b/views/components/player/player.templ index 029292e7..f954ba8b 100644 --- a/views/components/player/player.templ +++ b/views/components/player/player.templ @@ -16,8 +16,6 @@ templ PlayerSticky() { > /// - - + } diff --git a/views/pages/about.templ b/views/pages/about.templ index d3de1681..dd3efae6 100644 --- a/views/pages/about.templ +++ b/views/pages/about.templ @@ -52,12 +52,13 @@ templ About() {