This project is not affiliated with YouTube or Google, or anyone to that matter in any sort of ways.
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 }
+ } }
+ for _, tok := range strings.Split(msg, " ") {
+ if tok == "\n" || tok == "
" {
+
+ } else {
+ { tok + " " }
+ }
+ }
+
This project is not affiliated with YouTube or Google, or anyone to that matter in any sort of ways.