diff --git a/README.md b/README.md index b06294f..4be8841 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ IDK, it would be really nice of you to contribute, check the poorly written [CON - [ ] View/Edit queue - [ ] Expandable player - [x] Mobile - - [ ] Desktop + - [ ] Desktop - [ ] Share songs - [ ] Player's menu - [ ] Import **public** playlists from YouTube @@ -86,7 +86,6 @@ docker compose up -f docker-compose-dev.yml ## Acknowledgements - **This project is not affiliated with YouTube or Google, or anyone to that matter in any sort of ways.** -- The background was taken from [dankpods.net](https://dankpods.net) - Frank’s original image was taken from [dingusland.biz](https://dingusland.biz) - Colorscheme is inspired from [Dankpods](https://www.youtube.com/@DankPods) - youtube-scrape was used to search videos without using the actual YouTube API (small quota): MIT licenses by [Herman Fassett](https://github.com/HermanFassett) diff --git a/app/handlers/apis/email_login.go b/app/handlers/apis/email_login.go index 24cec19..028e8fe 100644 --- a/app/handlers/apis/email_login.go +++ b/app/handlers/apis/email_login.go @@ -37,7 +37,7 @@ func (e *emailLoginApi) HandleEmailLogin(w http.ResponseWriter, r *http.Request) log.Errorf("[EMAIL LOGIN API]: Failed to login user: %+v, error: %s\n", reqBody, err.Error()) // w.WriteHeader(http.StatusInternalServerError) status. - GenericError(fmt.Sprintf("No account associated with the email \"%s\" was found", reqBody.Email)). + BugsBunnyError(fmt.Sprintf("No account associated with the email \"%s\" was found", reqBody.Email)). Render(context.Background(), w) return } @@ -66,7 +66,7 @@ func (e *emailLoginApi) HandleEmailSignup(w http.ResponseWriter, r *http.Request log.Errorf("[EMAIL LOGIN API]: Failed to sign up a new user: %+v, error: %s\n", reqBody, err.Error()) // w.WriteHeader(http.StatusConflict) status. - GenericError(fmt.Sprintf("An account associated with the email \"%s\" already exists", reqBody.Email)). + BugsBunnyError(fmt.Sprintf("An account associated with the email \"%s\" already exists", reqBody.Email)). Render(context.Background(), w) return } @@ -95,14 +95,14 @@ func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *htt if err != nil { // w.Write([]byte("Invalid verification token")) status. - GenericError("Invalid verification token"). + BugsBunnyError("Invalid verification token"). Render(context.Background(), w) return } if verificationToken.Expires.After(time.Now().UTC()) { // w.Write([]byte("Expired verification token")) status. - GenericError("Expired verification token"). + BugsBunnyError("Expired verification token"). Render(context.Background(), w) return } @@ -113,7 +113,7 @@ func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *htt log.Error(err) // w.WriteHeader(http.StatusBadRequest) status. - GenericError("Invalid verification token"). + BugsBunnyError("Invalid verification token"). Render(context.Background(), w) return } @@ -121,13 +121,13 @@ func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *htt sessionToken, err := e.service.VerifyOtp(verificationToken.Value, reqBody) if errors.Is(err, login.ErrExpiredVerificationCode) { status. - GenericError("Expired verification code!"). + BugsBunnyError("Expired verification code!"). Render(context.Background(), w) return } if errors.Is(err, login.ErrInvalidVerificationCode) { status. - GenericError("Invalid verification code!"). + BugsBunnyError("Invalid verification code!"). Render(context.Background(), w) return } @@ -135,7 +135,7 @@ func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *htt log.Error(err) // w.WriteHeader(http.StatusInternalServerError) status. - GenericError("Something went wrong..."). + BugsBunnyError("Something went wrong..."). Render(context.Background(), w) return } diff --git a/app/handlers/pages/pages.go b/app/handlers/pages/pages.go index 2d7cbf5..84f9a7a 100644 --- a/app/handlers/pages/pages.go +++ b/app/handlers/pages/pages.go @@ -13,6 +13,7 @@ import ( "dankmuzikk/services/playlists" "dankmuzikk/services/youtube/download" "dankmuzikk/services/youtube/search" + "dankmuzikk/views/components/status" "dankmuzikk/views/layouts" "dankmuzikk/views/pages" "errors" @@ -21,10 +22,6 @@ import ( _ "github.com/a-h/templ" ) -const ( - notFoundMessage = "🤷‍♂️ I have no idea about what you requested!" -) - type pagesHandler struct { profileRepo db.GetterRepo[models.Profile] playlistsService *playlists.Service @@ -89,7 +86,9 @@ func (p *pagesHandler) HandleLoginPage(w http.ResponseWriter, r *http.Request) { func (p *pagesHandler) HandlePlaylistsPage(w http.ResponseWriter, r *http.Request) { profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) if !profileIdCorrect { - w.Write([]byte(notFoundMessage)) + status. + BugsBunnyError("I'm not sure what you're trying to do :)"). + Render(context.Background(), w) return } @@ -110,27 +109,46 @@ func (p *pagesHandler) HandlePlaylistsPage(w http.ResponseWriter, r *http.Reques func (p *pagesHandler) HandleSinglePlaylistPage(w http.ResponseWriter, r *http.Request) { profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) if !profileIdCorrect { - w.Write([]byte(notFoundMessage)) + status. + BugsBunnyError("I'm not sure what you're trying to do :)"). + Render(context.Background(), w) return } playlistPubId := r.PathValue("playlist_id") if playlistPubId == "" { - w.Write([]byte(notFoundMessage)) + status. + BugsBunnyError("You need to provide a playlist id!"). + Render(context.Background(), w) return } playlist, permission, err := p.playlistsService.Get(playlistPubId, profileId) + htmxReq := handlers.IsNoLayoutPage(r) switch { case errors.Is(err, playlists.ErrUnauthorizedToSeePlaylist): log.Errorln(err) - w.Write([]byte(notFoundMessage)) + if htmxReq { + status. + BugsBunnyError("You can't see this playlist!
(don't snoop around other people's stuff or else!)"). + Render(context.Background(), w) + } else { + layouts.Default("Error", + status. + BugsBunnyError("You can't see this playlist!
(don't snoop around other people's stuff or else!)")). + Render(r.Context(), w) + } return case err != nil: - if playlist.Title == "" { - log.Errorln(err) - w.Write([]byte(notFoundMessage)) - return + if htmxReq { + status. + BugsBunnyError("You can't see this playlist!
(it might be John Cena)"). + Render(context.Background(), w) + } else { + layouts.Default("Error", + status. + BugsBunnyError("You can't see this playlist!
(it might be John Cena)")). + Render(r.Context(), w) } } ctx := context.WithValue(r.Context(), handlers.PlaylistPermission, permission) diff --git a/app/static/css/themes/black.css b/app/static/css/themes/black.css index 4aec6f0..c0599c9 100644 --- a/app/static/css/themes/black.css +++ b/app/static/css/themes/black.css @@ -16,6 +16,5 @@ } body { - background-image: url("/static/images/dankground-dark.svg"); - background-color: var(--primary-color); + background-color: #131313; } diff --git a/app/static/css/themes/default.css b/app/static/css/themes/default.css index 309b2d0..7588bd6 100644 --- a/app/static/css/themes/default.css +++ b/app/static/css/themes/default.css @@ -16,6 +16,5 @@ } body { - background-image: url("/static/images/dankground.svg"); - background-color: var(--primary-color); + background-color: #305922; } diff --git a/app/static/css/themes/white.css b/app/static/css/themes/white.css index 327ae93..233cfae 100644 --- a/app/static/css/themes/white.css +++ b/app/static/css/themes/white.css @@ -16,6 +16,5 @@ } body { - background-image: url("/static/images/dankground-light.svg"); - background-color: var(--primary-color); + background-color: #dedede; } diff --git a/app/static/images/dankground-dark.svg b/app/static/images/dankground-dark.svg deleted file mode 100644 index d2b587b..0000000 --- a/app/static/images/dankground-dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/app/static/images/dankground-light.svg b/app/static/images/dankground-light.svg deleted file mode 100644 index fc6b5c6..0000000 --- a/app/static/images/dankground-light.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/app/static/images/dankground.svg b/app/static/images/dankground.svg deleted file mode 100644 index 5cf167a..0000000 --- a/app/static/images/dankground.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/static/images/error-img.webp b/app/static/images/error-img.webp new file mode 100644 index 0000000..df93809 Binary files /dev/null and b/app/static/images/error-img.webp differ diff --git a/app/static/js/player.js b/app/static/js/player.js index 54e9d2a..9601ae4 100644 --- a/app/static/js/player.js +++ b/app/static/js/player.js @@ -41,6 +41,7 @@ const playPauseToggleExapndedEl = document.getElementById("play-expand"), * @property {number} play_times * @property {string} added_at * @property {number} votes + * @property {number} order */ /** @@ -55,6 +56,7 @@ const playPauseToggleExapndedEl = document.getElementById("play-expand"), * @typedef {object} PlayerState * @property {LoopMode} loopMode * @property {boolean} shuffled + * @property {string} shuffledPlaylist * @property {Playlist} playlist * @property {number} currentSongIdx */ @@ -74,6 +76,7 @@ const LOOP_MODES = Object.freeze({ const playerState = { loopMode: LOOP_MODES.OFF, shuffled: false, + shuffledPlaylist: "", currentSongIdx: 0, playlist: { title: "Queue", @@ -227,13 +230,84 @@ function stopper(audioEl) { * @returns {Function} */ function shuffler(state) { - return () => { + // using Fisher–Yates shuffling algorithm https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + const __shuffleArray = (a) => { + let currIdx = a.length; + while (currIdx != 0) { + let randIdx = Math.floor(Math.random() * currIdx); + currIdx--; + [a[currIdx], a[randIdx]] = [a[randIdx], a[currIdx]]; + } + }; + + /** + * @param {string} songYtId + */ + const __shuffle = (songYtId) => { + if (isSingleSong()) { + return; + } + state.shuffledPlaylist = state.playlist.public_id; + const extraSongs = []; + state.playlist.songs.forEach((s) => { + for (let i = 0; i < s.votes - 1; i++) { + extraSongs.push(s); + } + }); + state.playlist.songs.push(...extraSongs); + __shuffleArray(state.playlist.songs); + let sIdx = 0; + if (!!audioPlayerEl.src) { + sIdx = state.playlist.songs.findIndex((s) => s.yt_id === songYtId); + if (sIdx !== -1) { + [state.playlist.songs[sIdx], state.playlist.songs[0]] = [ + state.playlist.songs[0], + state.playlist.songs[sIdx], + ]; + } + } + state.currentSongIdx = 0; + }; + + const __toggleShuffle = () => { state.shuffled = !state.shuffled; + if (state.shuffled) { + shuffle( + audioPlayerEl.src.substring( + audioPlayerEl.src.lastIndexOf("/") + 1, + audioPlayerEl.src.length - 4, + ), + ); + } else { + const tmp = state.playlist.songs.sort((si, sj) => si.order - sj.order); + state.playlist.songs = []; + for (let i = 0; i < tmp.length - 1; i++) { + if (tmp[i].yt_id === tmp[i + 1].yt_id) { + continue; + } + state.playlist.songs.push(tmp[i]); + } + if (tmp[tmp.length - 1].yt_id !== tmp[tmp.length - 2]) { + state.playlist.songs.push(tmp[tmp.length - 1]); + } + if (!!audioPlayerEl.src) { + state.currentSongIdx = state.playlist.songs.findIndex( + (s) => + s.yt_id === + audioPlayerEl.src.substring( + audioPlayerEl.src.lastIndexOf("/") + 1, + audioPlayerEl.src.length - 4, + ), + ); + } + } setPlayerButtonIcon( shuffleEl, state.shuffled ? Player.icons.shuffle : Player.icons.shuffleOff, ); }; + + return [__shuffle, __toggleShuffle]; } /** @@ -269,7 +343,10 @@ function playlister(state) { return; } // chack votes to whether repeat the song or not. - if (state.playlist.songs[state.currentSongIdx].votes > 1) { + if ( + state.playlist.songs[state.currentSongIdx].votes > 1 && + !state.shuffled + ) { const songToPlay = state.playlist.songs[state.currentSongIdx]; songToPlay.votes--; songToPlay.plays++; @@ -280,7 +357,6 @@ function playlister(state) { if ( !checkLoop(LOOP_MODES.ALL) && - !state.shuffled && state.currentSongIdx + 1 >= state.playlist.songs.length ) { stopMuzikk(); @@ -288,15 +364,15 @@ function playlister(state) { for (const s of state.playlist.songs) { if (!!s.plays) { s.votes = s.plays; + s.plays = 0; } } return; } - state.currentSongIdx = state.shuffled - ? Math.floor(Math.random() * state.playlist.songs.length) - : checkLoop(LOOP_MODES.ALL) && - state.currentSongIdx + 1 >= state.playlist.songs.length + state.currentSongIdx = + checkLoop(LOOP_MODES.ALL) && + state.currentSongIdx + 1 >= state.playlist.songs.length ? 0 : state.currentSongIdx + 1; const songToPlay = state.playlist.songs[state.currentSongIdx]; @@ -312,7 +388,10 @@ function playlister(state) { return; } // chack votes to whether repeat the song or not. - if (state.playlist.songs[state.currentSongIdx].votes > 1) { + if ( + state.playlist.songs[state.currentSongIdx].votes > 1 && + !state.shuffled + ) { const songToPlay = state.playlist.songs[state.currentSongIdx]; songToPlay.votes--; songToPlay.plays++; @@ -320,23 +399,19 @@ function playlister(state) { __setSongInPlaylistStyle(songToPlay.yt_id, state.playlist); return; } - if ( - !checkLoop(LOOP_MODES.ALL) && - !state.shuffled && - state.currentSongIdx - 1 < 0 - ) { + if (!checkLoop(LOOP_MODES.ALL) && state.currentSongIdx - 1 < 0) { stopMuzikk(); // reset songs' votes for (const s of state.playlist.songs) { if (!!s.plays) { s.votes = s.plays; + s.plays = 0; } } return; } - state.currentSongIdx = state.shuffled - ? Math.floor(Math.random() * state.playlist.songs.length) - : checkLoop(LOOP_MODES.ALL) && state.currentSongIdx - 1 < 0 + state.currentSongIdx = + checkLoop(LOOP_MODES.ALL) && state.currentSongIdx - 1 < 0 ? state.playlist.songs.length - 1 : state.currentSongIdx - 1; const songToPlay = state.playlist.songs[state.currentSongIdx]; @@ -565,20 +640,29 @@ function playSingleSong(song) { * @param {Playlist} playlist */ function playSongFromPlaylist(songYtId, playlist) { + if ( + playerState.shuffled && + playerState.shuffledPlaylist !== playlist.public_id + ) { + playerState.playlist = playlist; + shuffle(songYtId); + } const songIdx = playlist.songs.findIndex((s) => s.yt_id === songYtId); if (songIdx < 0) { alert("Invalid song!"); return; } - playerState.playlist = playlist; - playerState.playlist.songs = playlist.songs.map((s) => { - return { ...s, plays: 0 }; - }); + if (!playerState.shuffled) { + playerState.playlist = playlist; + playerState.playlist.songs = playlist.songs.map((s, idx) => { + return { ...s, plays: 0, order: idx }; + }); + } playerState.currentSongIdx = songIdx; if (playerState.playlist.songs[songIdx].votes === 0) { playerState.currentSongIdx++; } - const songToPlay = playlist.songs[songIdx]; + const songToPlay = playlist.songs[playerState.currentSongIdx]; highlightSongInPlaylist(songToPlay.yt_id, playlist); playSong(songToPlay); } @@ -634,7 +718,7 @@ function setMediaSessionMetadata(song) { const [toggleLoop, handleLoop, checkLoop] = looper(); const [playMuzikk, pauseMuzikk, togglePP] = playPauser(audioPlayerEl); const stopMuzikk = stopper(audioPlayerEl); -const toggleShuffle = shuffler(playerState); +const [shuffle, toggleShuffle] = shuffler(playerState); const [ nextMuzikk, previousMuzikk, diff --git a/app/views/components/song/song.templ b/app/views/components/song/song.templ index 3de6e7f..ebe1174 100644 --- a/app/views/components/song/song.templ +++ b/app/views/components/song/song.templ @@ -58,7 +58,9 @@ templ Song(s entities.Song, additionalData []string, additionalOptions []templ.C

By { s.Artist }

for _, info := range additionalData { -

{ info }

+ if info != "" { +

{ info }

+ } }
diff --git a/app/views/components/status/bugs_bunny_error.templ b/app/views/components/status/bugs_bunny_error.templ new file mode 100644 index 0000000..bd82a75 --- /dev/null +++ b/app/views/components/status/bugs_bunny_error.templ @@ -0,0 +1,18 @@ +package status + +import "strings" + +templ BugsBunnyError(msg string) { +
+ Error image +

+ for _, tok := range strings.Split(msg, " ") { + if tok == "\n" || tok == "
" { +
+ } else { + { tok + " " } + } + } +

+
+} diff --git a/app/views/components/themeswitch/themeswitch.templ b/app/views/components/themeswitch/themeswitch.templ index 609e0d7..1c930f2 100644 --- a/app/views/components/themeswitch/themeswitch.templ +++ b/app/views/components/themeswitch/themeswitch.templ @@ -64,7 +64,7 @@ script changeTheme(themeName string) { accent20: "#00000033", accent30: "#0000004c", accent69: "#000000b0", - bg: "/static/images/dankground.svg", + bg: "#305922", }, black: { primary: "#000000", @@ -79,7 +79,7 @@ script changeTheme(themeName string) { accent20: "#d3fcbf33", accent30: "#d3fcbf4C", accent69: "#d3fcbfB0", - bg: "/static/images/dankground-dark.svg", + bg: "#131313", }, white: { primary: "#ffffff", @@ -94,7 +94,7 @@ script changeTheme(themeName string) { accent20: "#d5ffc133", accent30: "#d5ffc14c", accent69: "#d5ffc1b0", - bg: "/static/images/dankground-light.svg", + bg: "#ededed", }, }; @@ -116,7 +116,6 @@ script changeTheme(themeName string) { style.setProperty('--accent-color-20', theme.accent20); style.setProperty('--accent-color-30', theme.accent30); style.setProperty('--accent-color-69', theme.accent69); - document.body.style.backgroundImage = `url('${theme.bg}')`; - document.body.style.backgroundColor = theme.primary; + document.body.style.backgroundColor = theme.bg; document.getElementById("popover-theme-switcher").style.display = "none"; } diff --git a/app/views/layouts/default.templ b/app/views/layouts/default.templ index 87daee5..5aceb7e 100644 --- a/app/views/layouts/default.templ +++ b/app/views/layouts/default.templ @@ -47,16 +47,7 @@ templ Default(title string, children ...templ.Component) { @header.Header()
diff --git a/app/views/layouts/raw.templ b/app/views/layouts/raw.templ index a6317fc..8533a94 100644 --- a/app/views/layouts/raw.templ +++ b/app/views/layouts/raw.templ @@ -44,16 +44,7 @@ templ Raw(title string, children ...templ.Component) {
for _, child := range children { diff --git a/app/views/pages/about.templ b/app/views/pages/about.templ index 6f66ffa..c97537d 100644 --- a/app/views/pages/about.templ +++ b/app/views/pages/about.templ @@ -68,7 +68,6 @@ templ aboutContent() {
  • This project is not affiliated with YouTube or Google, or anyone to that matter in any sort of ways.

  • -
  • The background was taken from dankpods.net
  • Frank’s original image was taken from dingusland.biz
  • Colorscheme is inspired from @Dankpods
  • youtube-scrape was used to search videos without using the actual YouTube API (small quota): MIT licenses by Herman Fassett.
  • diff --git a/app/views/pages/playlist.templ b/app/views/pages/playlist.templ index 621c787..5bbb223 100644 --- a/app/views/pages/playlist.templ +++ b/app/views/pages/playlist.templ @@ -258,10 +258,13 @@ css songThumb(url string) { } func playedTimes(times int) string { - if times == 1 { + switch { + case times == 1: return "Played once" - } else { + case times > 1: return fmt.Sprintf("Played %d times", times) + default: + return "" } }