Compare commits

..

3 Commits

Author SHA1 Message Date
34e024979f Load images into UI 2024-05-29 18:06:34 -05:00
f05febe5d1 Cache album covers rewritten 2024-05-29 17:19:57 -05:00
beca906979 Load & cache album covers 2024-05-29 16:25:33 -05:00
10 changed files with 192 additions and 21 deletions

135
albumCover.go Normal file
View File

@ -0,0 +1,135 @@
package main
import (
"errors"
"fmt"
"log"
"os"
"sync"
"github.com/adrg/xdg"
)
// Stores information about the album covers cache
type AlbumCoverInfo struct {
Cached bool
Error error
WaitGroup *sync.WaitGroup
}
// Caches info about the album covers
var albumCoverCacheInfo = make(map[string]*AlbumCoverInfo)
var cacheMutex = sync.RWMutex{}
func generateCacheFilename(albumId string) string {
albumCacheFile, err := xdg.CacheFile(fmt.Sprintf("%s/%s", appname, albumId))
if err != nil {
panic(fmt.Sprint("error creating cacheFile url: ", err))
}
return albumCacheFile
}
// Loads a single album cover and caches it in XDG_CACHE_HOME
// First it checks if the cover already exists in XDG_CACHE_HOME
// If it doesn't, loads it and stores it
func loadAlbumCover(albumId string) {
// Check cache info
cacheMutex.Lock()
_, ok := albumCoverCacheInfo[albumId]
cacheMutex.Unlock()
if ok {
log.Print("album cover: cache hit: ", albumId)
return
}
albumCacheFile := generateCacheFilename(albumId)
// Attempt to read file
if _, err := os.Stat(albumCacheFile); err == nil {
// File exists
log.Print("album cover: cache hit (disk): ", albumId)
cacheMutex.Lock()
albumCoverCacheInfo[albumId] = &AlbumCoverInfo{
Cached: true,
WaitGroup: &sync.WaitGroup{},
}
cacheMutex.Unlock()
return
}
// Load cover from network
log.Print("load album cover for ", albumId)
coverInfo := AlbumCoverInfo{
Cached: false,
WaitGroup: &sync.WaitGroup{},
}
coverInfo.WaitGroup.Add(1)
defer coverInfo.WaitGroup.Done()
cacheMutex.Lock()
albumCoverCacheInfo[albumId] = &coverInfo
cacheMutex.Unlock()
response, err := client.R().
// TODO: replace `fernando` with the username
Get(fmt.Sprintf(
"%s/rest/getCoverArt.view?id=%s&u=%s&s=12e7f3&t=%s&v=1.13.0&c=wmusic&size=300",
serverUrl,
albumId,
"fernando",
"d7bbe92d7da363aa202ae16136887adc",
))
if err != nil {
log.Print("error loadAlbumCover: ", err)
return
}
if !response.IsSuccess() {
log.Print("error loadAlbumCover")
log.Printf("%s", response.Body())
coverInfo.Error = errors.New("error loading")
return
}
imgBytes := response.Body()
// Write the image to cache
log.Printf("write %s: %+v", albumId, imgBytes[:10])
err = os.WriteFile(albumCacheFile, imgBytes, 0644)
if err != nil {
coverInfo.Error = errors.New("error writing album cover to disk")
log.Fatalf("Error writing to cache file for album cover: %s", albumCacheFile)
return
}
log.Print("Loading albumCover for ", albumId, " successful")
}
// Tries to load the album cover
func (a *App) GetAlbumCover(albumId string) ([]byte, error) {
cacheMutex.Lock()
coverInfo, ok := albumCoverCacheInfo[albumId]
cacheMutex.Unlock()
if !ok {
panic("Illegal state: Tried to load an album, but it wasn't on memory cache")
}
coverInfo.WaitGroup.Wait()
if coverInfo.Error != nil {
return nil, coverInfo.Error
}
// Read the file
filename := generateCacheFilename(albumId)
log.Print("reading: ", filename)
bytes, err := os.ReadFile(filename)
if err != nil {
log.Fatal("error getting album cover: ", err)
return nil, errors.New("error reading cover file")
}
log.Printf("%s : %+v", albumId, bytes[:10])
return bytes, nil
}

View File

@ -1,6 +1,6 @@
import { For, createResource, createSignal, onMount } from "solid-js"; import { For, createMemo, createResource, createSignal, onMount } from "solid-js";
import { GetRandomAlbums } from "../../wailsjs/go/main/App"; import { GetAlbumCover, GetRandomAlbums } from "../../wailsjs/go/main/App";
import {main} from "../../wailsjs/go/models"; import { main } from "../../wailsjs/go/models";
export function Home() { export function Home() {
const [hidden, setHidden] = createSignal(true); const [hidden, setHidden] = createSignal(true);
@ -8,13 +8,13 @@ export function Home() {
onMount(() => { onMount(() => {
// Fade in the UI // Fade in the UI
setTimeout(() => setHidden(false) , 150); setTimeout(() => setHidden(false), 150);
}); });
return ( return (
<div class={`min-h-screen ${hidden() ? "opacity-0" : "opacity-100"} transition-opacity`}> <div class={`min-h-screen ${hidden() ? "opacity-0" : "opacity-100"} transition-opacity`}>
<div class="py-10 h-64 overflow-scroll whitespace-nowrap"> <h1 class="font-black text-2xl pt-6 pb-4 pl-2">Random albums</h1>
<div class="pb-4 overflow-scroll whitespace-nowrap">
<For each={albums()}> <For each={albums()}>
{(album) => <Album album={album} />} {(album) => <Album album={album} />}
</For> </For>
@ -23,12 +23,22 @@ export function Home() {
); );
} }
function Album(props: {album: main.Album}) { function Album(props: { album: main.Album }) {
const [coverBytes] = createResource(async() => await GetAlbumCover(props.album.id));
const base64Image = createMemo(() => {
if (coverBytes.state !== "ready") return "";
// At runtime this is a string, not a number array
const bytes = coverBytes() as unknown as string;
return `data:;base64,${bytes}`;
});
return ( return (
<div class="inline-block mx-2 p-1 w-32 rounded bg-zinc-900"> <div class="inline-block mx-2 p-1 w-32 rounded bg-zinc-900">
<img <img
class="inline-block rounded w-30 h-30 min-w-30 min-h-30" class="inline-block rounded w-30 h-30 min-w-30 min-h-30"
src={props.album.mediumImageUrl} src={base64Image()}
alt="" alt=""
/> />
<br /> <br />

9
frontend/vite.config.mts Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
export default defineConfig({
plugins: [solidPlugin()],
build: {
target: "esnext",
},
});

View File

@ -1,9 +0,0 @@
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
plugins: [solidPlugin()],
build: {
target: 'esnext',
},
});

View File

@ -2,6 +2,8 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import {main} from '../models'; import {main} from '../models';
export function GetAlbumCover(arg1:string):Promise<Array<number>>;
export function GetRandomAlbums():Promise<Array<main.Album>>; export function GetRandomAlbums():Promise<Array<main.Album>>;
export function Greet(arg1:string):Promise<string>; export function Greet(arg1:string):Promise<string>;

View File

@ -2,6 +2,10 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
export function GetAlbumCover(arg1) {
return window['go']['main']['App']['GetAlbumCover'](arg1);
}
export function GetRandomAlbums() { export function GetRandomAlbums() {
return window['go']['main']['App']['GetRandomAlbums'](); return window['go']['main']['App']['GetRandomAlbums']();
} }

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.21
toolchain go1.22.3 toolchain go1.22.3
require ( require (
github.com/adrg/xdg v0.4.0
github.com/go-resty/resty/v2 v2.13.1 github.com/go-resty/resty/v2 v2.13.1
github.com/wailsapp/wails/v2 v2.8.2 github.com/wailsapp/wails/v2 v2.8.2
) )

3
go.sum
View File

@ -1,3 +1,5 @@
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -96,6 +98,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -13,14 +13,20 @@ type AuthError struct {
Error string `json:"error"` Error string `json:"error"`
} }
// Name of the app, used to create directories in XDG_<folder> and store files inside
const appname = "wMusic"
var LoggedUser AuthSuccess var LoggedUser AuthSuccess
var randomAlbumWaitGroup sync.WaitGroup var randomAlbumWaitGroup sync.WaitGroup
var randomAlbums []Album var randomAlbums []Album
var serverUrl = ""
var client = resty.New()
// (Tries to) login to a remote navidrome server // (Tries to) login to a remote navidrome server
func (a *App) Login(server, username, password string) (bool, error) { func (a *App) Login(server, username, password string) (bool, error) {
log.Print("begin Login to server") log.Print("begin Login to server")
client := resty.New() // client := resty.New()
// TODO: check server for leading https and trailing /, normalize // TODO: check server for leading https and trailing /, normalize
@ -42,6 +48,10 @@ func (a *App) Login(server, username, password string) (bool, error) {
if response.IsSuccess() { if response.IsSuccess() {
log.Printf("%+v", successData) log.Printf("%+v", successData)
// Set the auth header globally
client.SetHeader("x-nd-authorization", successData.Token)
serverUrl = server
// Set global session // Set global session
LoggedUser = successData LoggedUser = successData
// Begin to load the list of albums on the background // Begin to load the list of albums on the background
@ -89,7 +99,11 @@ func loadAlbums(serverUrl string) {
log.Print("Get albums success") log.Print("Get albums success")
// TODO: Begin to load album artwork in the background // TODO: Begin to load album artwork in the background
// Album artwork comes from the url /rest/getCoverArt.view // Album artwork comes from the url /rest/getCoverArt.view
} // Cache album images in XDG_CACHE_HOME
for _, album := range randomAlbums {
albumId := album.ID
// TODO: Do the loading go loadAlbumCover(albumId)
}
}
} }

View File

@ -1,6 +1,8 @@
package main package main
import "time" import (
"time"
)
// The result of a Login // The result of a Login
type AuthSuccess struct { type AuthSuccess struct {