Compare commits
3 Commits
fdfde86360
...
8f6baeba91
Author | SHA1 | Date | |
---|---|---|---|
8f6baeba91 | |||
5d91aad2f2 | |||
da39b305c8 |
@ -3,6 +3,7 @@ package album
|
|||||||
import (
|
import (
|
||||||
"acide/src/modules/song"
|
"acide/src/modules/song"
|
||||||
"acide/src/utils"
|
"acide/src/utils"
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@ -15,6 +16,7 @@ type ClientSong struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Artist string `json:"artist"`
|
Artist string `json:"artist"`
|
||||||
AlbumId string `json:"albumId"`
|
AlbumId string `json:"albumId"`
|
||||||
|
Album string `json:"album"`
|
||||||
SongId string `json:"songId"`
|
SongId string `json:"songId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,21 +32,10 @@ func allAlbumsPage(c echo.Context) error {
|
|||||||
// if there's a search query, do that
|
// if there's a search query, do that
|
||||||
searchQuery := c.QueryParam("s")
|
searchQuery := c.QueryParam("s")
|
||||||
isHtmxRequest := c.Request().Header.Get("HX-Request") == "true"
|
isHtmxRequest := c.Request().Header.Get("HX-Request") == "true"
|
||||||
|
|
||||||
// get the first 10 albums
|
|
||||||
token, server := utils.Credentials(c)
|
token, server := utils.Credentials(c)
|
||||||
|
|
||||||
var (
|
// if searchQuery is empty, this will get the first 30 albums
|
||||||
albums []utils.Album
|
albums, err := searchAlbums(token, searchQuery, server, 0, 30)
|
||||||
err error
|
|
||||||
)
|
|
||||||
if searchQuery != "" {
|
|
||||||
// search for the requested albums
|
|
||||||
albums, err = searchAlbums(token, searchQuery, server, 0, 20)
|
|
||||||
} else {
|
|
||||||
// get 10 random albums
|
|
||||||
albums, err = loadAlbums(token, server, 0, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -99,20 +90,25 @@ func albumPage(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convert the song list to json
|
// convert the song list to json
|
||||||
clientSons := make([]ClientSong, len(songs))
|
clientSongs := make([]ClientSong, len(songs))
|
||||||
for i, song := range songs {
|
for i, song := range songs {
|
||||||
clientSons[i] = ClientSong{
|
clientSongs[i] = ClientSong{
|
||||||
Title: song.Title,
|
Title: song.Title,
|
||||||
Artist: song.Artist,
|
Artist: song.Artist,
|
||||||
AlbumId: album.ID,
|
AlbumId: album.ID,
|
||||||
|
Album: album.Name,
|
||||||
SongId: song.ID,
|
SongId: song.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clientSongsJson, err := json.Marshal(clientSons)
|
|
||||||
if err != nil {
|
var buff bytes.Buffer
|
||||||
|
enc := json.NewEncoder(&buff)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
if err := enc.Encode(&clientSongs); err != nil {
|
||||||
log.Printf("Error marshaling clientSongs: %s", err)
|
log.Printf("Error marshaling clientSongs: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
clientSongsJson := buff.String()
|
||||||
|
|
||||||
return utils.RenderTempl(c, http.StatusOK, albumTempl(albumId, album, songs, string(clientSongsJson)))
|
return utils.RenderTempl(c, http.StatusOK, albumTempl(albumId, album, songs, string(clientSongsJson)))
|
||||||
}
|
}
|
||||||
|
@ -33,28 +33,6 @@ func searchAlbums(token, query, server string, start, end int) ([]utils.Album, e
|
|||||||
return albums, nil
|
return albums, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAlbums(token, server string, start, end int) ([]utils.Album, error) {
|
|
||||||
var albums []utils.Album
|
|
||||||
var error utils.NavError
|
|
||||||
|
|
||||||
client := resty.New()
|
|
||||||
response, err := client.R().
|
|
||||||
SetHeader("x-nd-authorization", fmt.Sprintf("Bearer %s", token)).
|
|
||||||
SetResult(&albums).
|
|
||||||
SetError(&error).
|
|
||||||
Get(fmt.Sprintf("%s/api/album?_start=%d&_end=%d&_sort=name&_order=ASC", server, start, end))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !response.IsSuccess() {
|
|
||||||
return nil, errors.New(error.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return albums, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadAlbum(token, server, albumId string) (*utils.Album, error) {
|
func loadAlbum(token, server, albumId string) (*utils.Album, error) {
|
||||||
var album utils.Album
|
var album utils.Album
|
||||||
var error utils.NavError
|
var error utils.NavError
|
||||||
|
@ -33,10 +33,16 @@ templ MusicPlayer() {
|
|||||||
class="fixed bottom-0 left-0 w-screen border-t border-sky-500 grid grid-cols-[3rem_auto_3rem_3rem] gap-2 p-1 bg-white"
|
class="fixed bottom-0 left-0 w-screen border-t border-sky-500 grid grid-cols-[3rem_auto_3rem_3rem] gap-2 p-1 bg-white"
|
||||||
x-data="player"
|
x-data="player"
|
||||||
>
|
>
|
||||||
<div class="h-12 bg-sky-200 rounded">
|
<div
|
||||||
|
class="h-12 bg-sky-200 rounded"
|
||||||
|
_="on click toggle .hidden on #full-music-player"
|
||||||
|
>
|
||||||
<img class="rounded" id="music-player-img" :src="queue[idx]? `/covers/${queue[idx]?.albumId}` : ''"/>
|
<img class="rounded" id="music-player-img" :src="queue[idx]? `/covers/${queue[idx]?.albumId}` : ''"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-hidden">
|
<div
|
||||||
|
class="w-full overflow-hidden"
|
||||||
|
_="on click toggle .hidden on #full-music-player"
|
||||||
|
>
|
||||||
<p
|
<p
|
||||||
id="music-player-title"
|
id="music-player-title"
|
||||||
class="overflow-hidden overflow-ellipsis whitespace-nowrap w-full"
|
class="overflow-hidden overflow-ellipsis whitespace-nowrap w-full"
|
||||||
@ -68,6 +74,7 @@ templ MusicPlayer() {
|
|||||||
>
|
>
|
||||||
@skipForwardIcon(24)
|
@skipForwardIcon(24)
|
||||||
</button>
|
</button>
|
||||||
|
@fullMusicPlayer()
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
Alpine.data("player", () => ({
|
Alpine.data("player", () => ({
|
||||||
@ -81,33 +88,74 @@ templ MusicPlayer() {
|
|||||||
volume: 0.1,
|
volume: 0.1,
|
||||||
playing: false,
|
playing: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
progress: 0,
|
||||||
|
|
||||||
|
// sets the queue, and plays the song at idx
|
||||||
replaceQueueAndPlayAt(queue, idx) {
|
replaceQueueAndPlayAt(queue, idx) {
|
||||||
this.queue = queue;
|
this.queue = queue;
|
||||||
this.idx = idx;
|
this.idx = idx;
|
||||||
this.play();
|
this.play(idx);
|
||||||
},
|
|
||||||
// Plays the song at the current position
|
// setup the preload and progress listener
|
||||||
async play() {
|
console.log("setting up listener");
|
||||||
const songId = this.queue[this.idx].songId;
|
setInterval(() => this.checkDuration(), 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Plays the song at the passed idx, sets this.idx to that,
|
||||||
|
// and plays a preloaded song
|
||||||
|
//
|
||||||
|
// If preloaded=true, this function will assume that it is the
|
||||||
|
// next song. It will trust that idx is correct.
|
||||||
|
async play(idx) {
|
||||||
|
const preloaded = this.nextSound !== null && idx === this.idx + 1;
|
||||||
|
|
||||||
|
// if a song is currently playing
|
||||||
|
// then fade it out before playing the next sound
|
||||||
|
if (this.playing === true
|
||||||
|
&& this.currentSound !== null
|
||||||
|
) {
|
||||||
|
// this will not trigger when next() is called,
|
||||||
|
// because next() sets this.playing=false
|
||||||
|
|
||||||
if (this.currentSound !== null) {
|
|
||||||
this.currentSound.fade(this.volume, 0.0, 250);
|
this.currentSound.fade(this.volume, 0.0, 250);
|
||||||
await wait(250);
|
await wait(250);
|
||||||
this.currentSound.unload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentSound?.unload?.();
|
||||||
|
this.playing = false;
|
||||||
|
this.currentSound = null;
|
||||||
|
this.idx = idx;
|
||||||
|
|
||||||
|
// if a song is preloaded, assume it's the next song and play it
|
||||||
|
if (preloaded === true && this.nextSound !== null) {
|
||||||
|
this.currentSound = this.nextSound;
|
||||||
|
this.nextSound = null;
|
||||||
|
this.currentSound.play();
|
||||||
|
this.playing = true;
|
||||||
|
} else {
|
||||||
|
// otherwise, load the song at idx and play it
|
||||||
|
const songId = this.queue[idx].songId;
|
||||||
|
|
||||||
const sound = new Howl({
|
const sound = new Howl({
|
||||||
src: `https://navidrome.araozu.dev/rest/stream.view?id=${songId}&v=1.13.0&c=music-to-go&u=fernando&s=49805d&t=4148cd1c83ae1bd01334facf4e70a947`,
|
src: `https://navidrome.araozu.dev/rest/stream.view?id=${songId}&v=1.13.0&c=music-to-go&u=fernando&s=49805d&t=4148cd1c83ae1bd01334facf4e70a947`,
|
||||||
html5: true,
|
html5: true,
|
||||||
volume: this.volume,
|
volume: this.volume,
|
||||||
})
|
});
|
||||||
|
this.currentSound = sound;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
sound.play();
|
sound.play();
|
||||||
let preloadInterval;
|
|
||||||
sound.once("load", () => {
|
sound.once("load", () => {
|
||||||
|
this.loading = false;
|
||||||
|
this.playing = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set-up preloading for the next song
|
||||||
|
const sound = this.currentSound;
|
||||||
|
sound.once("play", () => {
|
||||||
const length = sound.duration();
|
const length = sound.duration();
|
||||||
const targetLength = length - 5;
|
const targetLength = length - 5;
|
||||||
|
let preloadInterval;
|
||||||
preloadInterval = setInterval(() => {
|
preloadInterval = setInterval(() => {
|
||||||
const pos = sound.seek();
|
const pos = sound.seek();
|
||||||
if (pos > targetLength) {
|
if (pos > targetLength) {
|
||||||
@ -115,22 +163,37 @@ templ MusicPlayer() {
|
|||||||
clearInterval(preloadInterval);
|
clearInterval(preloadInterval);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
this.loading = false;
|
|
||||||
this.playing = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// set-up playing the next song when the current finishes
|
||||||
sound.once("end", () => {
|
sound.once("end", () => {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
this.next();
|
this.next();
|
||||||
});
|
});
|
||||||
this.currentSound = sound;
|
this.currentSound = sound;
|
||||||
},
|
},
|
||||||
async playNext() {
|
|
||||||
this.currentSound?.unload();
|
// checks the duration of the playing song and:
|
||||||
this.nextSound.play();
|
// - updates the song progress (0-100%)
|
||||||
this.playing = true;
|
// - begins preloading
|
||||||
this.currentSound = this.nextSound;
|
checkDuration() {
|
||||||
this.nextSound = null;
|
const sound = this.currentSound;
|
||||||
|
if (this.playing) {
|
||||||
|
const length = sound.duration();
|
||||||
|
if (length <= 0) return;
|
||||||
|
|
||||||
|
const position = sound.seek();
|
||||||
|
|
||||||
|
// preload 5s before the song ends
|
||||||
|
if (position >= length - 5 && this.nextSound === null) {
|
||||||
|
this.preload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the song progress percentage
|
||||||
|
this.progress = Math.floor((position * 100) / length);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
togglePlayPause() {
|
togglePlayPause() {
|
||||||
if (this.playing === true) {
|
if (this.playing === true) {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
@ -142,8 +205,7 @@ templ MusicPlayer() {
|
|||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
if (this.idx + 1 < this.queue.length) {
|
if (this.idx + 1 < this.queue.length) {
|
||||||
this.idx += 1;
|
this.play(this.idx + 1);
|
||||||
this.playNext();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preload() {
|
preload() {
|
||||||
@ -159,30 +221,13 @@ templ MusicPlayer() {
|
|||||||
volume: 0,
|
volume: 0,
|
||||||
preload: true,
|
preload: true,
|
||||||
});
|
});
|
||||||
// Attempt to play immediately the song, immediately pause it, rewind it and set volume back up
|
// Attempt to immediately play the song, immediately pause it, rewind it and set volume back up
|
||||||
nextSound.play();
|
nextSound.play();
|
||||||
|
|
||||||
let preloadInterval;
|
let preloadInterval;
|
||||||
nextSound.once("load", () => {
|
nextSound.once("load", () => {
|
||||||
nextSound.pause();
|
nextSound.pause();
|
||||||
nextSound.seek(0);
|
nextSound.seek(0);
|
||||||
nextSound.volume(this.volume);
|
nextSound.volume(this.volume);
|
||||||
|
|
||||||
const length = nextSound.duration();
|
|
||||||
const targetLength = length - 5;
|
|
||||||
preloadInterval = setInterval(() => {
|
|
||||||
const pos = nextSound.seek();
|
|
||||||
if (pos > targetLength) {
|
|
||||||
this.preload();
|
|
||||||
clearInterval(preloadInterval);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
this.loading = false;
|
|
||||||
this.playing = true;
|
|
||||||
});
|
|
||||||
nextSound.once("end", () => {
|
|
||||||
this.playing = false;
|
|
||||||
this.next();
|
|
||||||
});
|
});
|
||||||
this.nextSound = nextSound;
|
this.nextSound = nextSound;
|
||||||
}
|
}
|
||||||
@ -196,6 +241,56 @@ templ MusicPlayer() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ fullMusicPlayer() {
|
||||||
|
<div id="full-music-player" class="fixed top-0 left-0 w-screen h-screen bg-white hidden">
|
||||||
|
<div class="flex justify-center py-6">
|
||||||
|
<div class="bg-sky-200 rounded w-[20rem] h-[20rem]">
|
||||||
|
<img
|
||||||
|
class="rounded inline-block w-full"
|
||||||
|
id="full-music-player-img"
|
||||||
|
:src="queue[idx]? `/covers/${queue[idx]?.albumId}` : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="px-6">
|
||||||
|
<progress
|
||||||
|
class="inline-block w-full"
|
||||||
|
id="song-progress"
|
||||||
|
aria-label="Song progress"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
:value="progress"
|
||||||
|
></progress>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="font-bold text-2xl"
|
||||||
|
x-text="queue[idx]?.title ?? '-'"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="text-lg"
|
||||||
|
x-text="queue[idx]?.album ?? '-'"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="text-sm opacity-75"
|
||||||
|
x-text="queue[idx]?.artist ?? '-'"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 w-full grid grid-cols-2 bg-sky-50">
|
||||||
|
<button type="button" class="inline-block text-center py-4">
|
||||||
|
@playlistIcon(24)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-block text-center py-4"
|
||||||
|
_="on click toggle .hidden on #full-music-player"
|
||||||
|
>
|
||||||
|
@caretDoubleDownIcon(24)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
templ playIcon(size int) {
|
templ playIcon(size int) {
|
||||||
<svg
|
<svg
|
||||||
id="play-icon"
|
id="play-icon"
|
||||||
@ -251,3 +346,33 @@ templ circleNotchIcon(size int) {
|
|||||||
<path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"></path>
|
<path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ playlistIcon(size int) {
|
||||||
|
<svg
|
||||||
|
class="inline-block"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="#000000"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
style="--darkreader-inline-fill: #000000;"
|
||||||
|
data-darkreader-inline-fill=""
|
||||||
|
width={ strconv.Itoa(size) }
|
||||||
|
height={ strconv.Itoa(size) }
|
||||||
|
>
|
||||||
|
<path d="M32,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H40A8,8,0,0,1,32,64Zm8,72H160a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16Zm72,48H40a8,8,0,0,0,0,16h72a8,8,0,0,0,0-16Zm135.66-57.7a8,8,0,0,1-10,5.36L208,122.75V192a32.05,32.05,0,1,1-16-27.69V112a8,8,0,0,1,10.3-7.66l40,12A8,8,0,0,1,247.66,126.3ZM192,192a16,16,0,1,0-16,16A16,16,0,0,0,192,192Z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ caretDoubleDownIcon(size int) {
|
||||||
|
<svg
|
||||||
|
class="inline-block"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="#000000"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
style="--darkreader-inline-fill: #000000;"
|
||||||
|
data-darkreader-inline-fill=""
|
||||||
|
width={ strconv.Itoa(size) }
|
||||||
|
height={ strconv.Itoa(size) }
|
||||||
|
>
|
||||||
|
<path d="M213.66,130.34a8,8,0,0,1,0,11.32l-80,80a8,8,0,0,1-11.32,0l-80-80a8,8,0,0,1,11.32-11.32L128,204.69l74.34-74.35A8,8,0,0,1,213.66,130.34Zm-91.32,11.32a8,8,0,0,0,11.32,0l80-80a8,8,0,0,0-11.32-11.32L128,124.69,53.66,50.34A8,8,0,0,0,42.34,61.66Z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user