Skip to content

Latest commit

 

History

History
284 lines (227 loc) · 13.5 KB

README.md

File metadata and controls

284 lines (227 loc) · 13.5 KB

Mélodie

GitHub All Releases GitHub release (latest by date including pre-releases) GitHub CI

Melodie is a portable, simple-as-pie music player.

There are thunsands of them in the wild. This mine is an excuse for learning Electron, Svelte and reactive programming.

Installation

You will find installers on the releases page.

Notes:

Windows installers are not signed. Windows will warn you that the source is insecure (it is not!). Simply confirm your choice to install or run Mélodie.

AppImage for linux, Snap and NSIS installer will automatically update to the latest available version.

If you run Mélodie from a zip or using Windows portable version, you will have to download updates by yourself

TODOs

features

  • images from tags
  • system integration (play folder/file)
  • configure replay gain from settings
  • display tracks/albums/artists count in settings
  • allow reseting database from settings
  • smaller screen support (UI refactor)

tools

release

  • packages
    • Linux snap
    • [-] Linux AppImage: no desktop menu icon (need AppImageLauncher)
    • [-] Windows Nsis: not signed, the executable is flaged as insecure
    • [-] Windows Portable: not signed, and does not update automatically
    • [-] MacOS DMG
    • MacOS zip
  • github page
  • usage statistics
  • references

Bugs and known issues

  1. Undetected live changes: remove tracks and re-add them. This is a linux-only issue with chokidar
  2. Files renamed or moved to other watched folders are removed and re-added. This is a limitation with chokidar
  3. When loading new folders, enqueuing or going to album details will give incomplete results. Going back and forth won't load new data
  4. Security: clean html in artist/album names (wrapWithRefs returns injectable markup)
  5. AppImage, when used with AppImageLauncher, fail to auto update: electron-userland/electron-builder#4046 (comment)
  6. If we knew current position in browser history, then we could disabled navigation button accordingly
  7. Page navigation: use:link doesn't work in tests and raise Svelte warning. a.href is fine
  8. Disklist/TrackTable dropdown does not consider scroll position (in storybook only)
  9. Testing input: fireEvent.change, input or keyUp does not trigger svelte's bind:value on input
  10. The test suite is becoming brittle
    1. Media service › triggerAlbumsEnrichment › saves first returned cover for album
      > 839 |       expect(await fs.readFile(savedAlbums[0].media, 'utf8')).toEqual(
          |                                                               ^
    2. Media service › triggerAlbumsEnrichment › retries album with no cover but at least one restriced provided Is a 1ms difference in expected processedEpoch
    3. TracksQueue component › given a list of tracks › clears tracks queue The list items are still visible after clear (probably because of the animation)
    4. AddToPlaylist component › given some playlists › saves new playlist with all tracks The dropdown menu is still visible (probably because of the animation)

Configuring logs

Log level file is .levels in the application userData folder. Its syntax is:

# this is a comment
logger-name=level
wildcard*=level

logger names are:

  • core
  • renderer
  • updater
  • services/ where is tracks, playlists, media, settings
  • providers/ where is local, audiodb, discogs
  • models/ where is tracks, albums, artists, playlists, settings

and levels are (in order): trace (most verbose), debug, info, warn, error, fatal, silent (no logs)

Wildcards can be at the beginning *tracks or the end models/*. In case a logger name is matching several directives, the first always wins.

You can edit the file, and trigger logger level refresh by sending SIGUSR2 to the application: kill -USR2 {pid} (first log issued contains pid)

Running locally

git clone git@github.com:feugy/melodie.git
cd melodie
npm i
npm run build

Testing

Core services network mocks (nocks)

Some services are hitting external APIs, such as AudioDB. As we don't want to flood them with test requests, these are using network mocks.

To use real services, run your tests with REAL_NETWORK environment variables (whatever its value). When using real services, update the mocks by defining UPDATE_NOCKS environment variables (whatever its value). Nocks will stay unchanged on test failure.

Some providers need access keys during tests. Just make a .env file in the root folder, with the appropriate values:

DISCOGS_TOKEN=XYZ
AUDIODB_KEY=1

Trying snaps out

Working with snaps locally isn't really easy.

  1. install the real app from the store:
    snap install melodie
  2. then package your app in debug mode, to access the unpacked snap:
    DEBUG=electron-builder npm run release:artifacts -- -l
  3. copy missing files to the unpacked snap, and keep your latest changes:
    mkdir dist/__snap-amd64/tmp
    mv dist/__snap-amd64/* dist/__snap-amd64/tmp
    cp -r /snap/melodie/current/* dist/__snap-amd64/
    cp -r dist/linux-unpacked/* dist/__snap-amd64/
    mv dist/__snap-amd64/tmp/* dist/__snap-amd64/*
  4. now use your development code:
    snap try dist/__snap-amd64
    melodie
  5. and revert when you're done:
    snap revert melodie

Releasing

Release process is fairly automated: it will generate changelog, bump version, and build melodie for different platform, creating several artifacts which are either packages (snap, AppImage, Nsis) or plain files (zip).

Theses artifacts will be either published on their respective store (snapcraft...) or uploaded to github as a release. Once a Github release is published, users who installed an auto-updatable package (snap, AppImage, Nsis) will get the new version auto-magically.

  1. When ready, bump the version on local machine:
    npm run release:bump
  2. Then push tags to github, as it'll trigger the artifact creation:
    git push --follow-tags
  3. Finally, go to github releases, and edit the newest one:
    1. give it a code name
    2. copy the latest section of the changelog in the release body
    3. don't forget to select "is a prerelease" checkbox in case it is one
    4. publish your release
    5. go and slack off!

Notable facts

  • Started with a search engine (FlexSearch) to store tracks, and serialized JS lists for albums & artists. Altough very performant (50s to index the whole music library), the memory footprint is heavy (700Mo) since FlexSearch is loading entire indices in memory

  • Moved to sqlite3 denormalized tables (drawback: no streaming supported)

  • Dropped the idea to query tracks of a given albums/artists/genre/playlist by using SQL queries. Sqlite has a very poor json support, compared to Postgres. There is only one way to query json field: json_extract. It is possible to create indexes on expressions, and this makes retrieving tracks of a given album very efficient:

    create index track_album on tracks (trim(lower(json_extract(tags, '$.album'))))
    select id, tags from tracks where trim(lower(json_extract(tags, '$.album'))) = lower('Le grand bleu')
    

    However, it doesn't work on artists or genres, because they are modeled with arrays, and operator used do not leverage any index:

    select id, tags from tracks where instr(lower(json_extract(tags, '$.artists')), 'eric serra')
    select id, tags from tracks where json_extract(tags, '$.artists') like '%eric serra%'
    
  • chokidar is the best of breed watch tool, but has this annoying linux-only big when moving folders outside of the watched paths Watchman is a C program that'll be hard to bundle. node-watch does not send file event when removing/renaming folders watchr API seems overly complex watch-pack is using chokidar and the next version isn't ready

  • wiring jest, storybook, svelte and tailwind was really painfull. Too many configuration files now :( To make storyshots working, I had to downgrade Jest because of an annoying bug (reference).

  • I considered Sapper for its nice conventional router, but given all the unsued feature (service workers, SSR) I chose a simpler router. It is based on hash handling, as electron urls are using file:// protocol which makes it difficult to use with history-based routers.

  • Initially, albums & artists id where hash of their names. It was very convenient to keep a list of artist's albums just by storing album names in artist's linked array. UI would infer ids by applying the same hash. However, it is common to see albums with same name from different artists (like "Greatest hits"). To mitigate this issue, I had to make album's id out of album name and album artist (when defined). This ruined the hash convention, and I had to replace all "links" by proper references (id + name). Now UI does not infer ids anymore.

  • For system notifications, document.hidden and visibilityChange are too weak because they only notice when the app is minimized/restored

  • System notification was tricky: HTML5 Notification API doesn't support actions, except from service workers. Using service workers was overkill, and didn't work in the end. Electron's native notificaiton does not support actions either. Using node-notifier was a viable possibility, but doesn't support actions in a portable fashion (notify-send on linux doesn't support it). Finally back to HTML5 notification API, without actions :(

  • The discovery of mediaSession's metadata and handler was completely random. It's only supported by Chrome (hopefully for me!), and can be seen on Deezer, Spotify or Youtube Music. However, it does not display artworks.

  • IntersectionObserver does not call the intersection entry when the position inside viewport is changing but the intersection doesn't. As a result, dropdown in the sheet will enter viewport during sheet animation, causing troubles positioning the menu

  • AC/DC was displayed as 2 different artists ('AC' and 'DC'). This is an issue with ID3 tags: version 2.3 uses / as a separators for artists. Overitting mp3 tags with 2.4 solved the issue

  • Snap packaging was hairy to figure out. It is clearly the best option on Linux, as it has great desktop integration (which AppImage lacks) and a renowed app store. However, getting the MediaMetadata to work with snap confinement took two days of try-and-fail research. The full journey is available in this PR on electron-builnder. Besides, the way snapd is creating different folders for each new version forced me to move artist albums outside of electron's data folders: snapd ensure that files are copied from old to new version, but can not update the media full paths store inside SQLite DB.

How watch & diff works

  • on app load, trigger diff
    1. get followed folders from store
    2. crawl followed folders, return array of paths + hashs + last changed
    3. get array of tracks with hash + last changed from DB
    4. compare to find new & changed hashes
      1. enrich with tags & media
      2. save
    5. compare to isolate deleted hashes
      1. remove corresponding tracks
  • while app is running
    1. watch new & changed paths
      1. compute hash, enrich with tags & media
      2. save
    2. watch deleted paths
      1. compute hash
      2. remove corresponding tracks
  • when adding new followed folder
    1. save in store
    2. crawl new folder, return array of paths
    3. compute hash, enrich with tags & media
    4. save

How missing artworks/covers retrieval works

  • on UI demand trigger process
    1. push all artists/albums without artwork/cover, and not process since N in a queue
    2. apply rate limit (to avoid flooding disks/providers)
    3. call providers one by one
      1. save first result as artwork/cover, stop
      2. on no results, but at least on provider returned rate limitation, enqueue artist/album
      3. on no results, save date on artist/album