[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Add update checking

Kasper (Oct 7, 2024, 8:53 AM +0200) e9a2706f 6fbc9fa4

+120 -9
+2 -1
generate-update-json.mjs
··· 7 7 8 8 const package_json = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'))) 9 9 10 + /** @type {import('./src/electron/update').UpdateJson} */ 10 11 const update_json = { 11 12 // In the future we can create a stable-1 channel, to avoid notifying users about versions they shouldn't upgrade to. For example if their OS becomes unsupported, or If we decide to drop version migration logic. 12 13 'stable-0': { 13 14 url: package_json.updates_url_prefix + '/v' + package_json.version, 14 - version: 'v' + package_json.version, 15 + version: package_json.version, 15 16 }, 16 17 } 17 18
+1
package.json
··· 7 7 "version": "0.18.0", 8 8 "repository": "https://github.com/probablykasper/ferrum", 9 9 "updates_url_prefix": "https://github.com/probablykasper/ferrum/releases", 10 + "latest-release-api-url": "https://api.github.com/repos/probablykasper/ferrum/releases/latest", 10 11 "scripts": { 11 12 "dev": "npm run napi:dev && vite dev", 12 13 "dev-release": "npm run napi && vite dev",
+1 -1
src-native/sort.rs
··· 114 114 if !options.sort_desc { 115 115 items.reverse(); 116 116 } 117 - println!("Sort: {:.3}ms", now.elapsed().as_secs_f32() * 1000.0); 117 + println!("Sort: {}ms", now.elapsed().as_millis()); 118 118 let item_ids = items.into_iter().map(|item| item.item_id).collect(); 119 119 return Ok(item_ids); 120 120 }
+12 -2
src/App.svelte
··· 9 9 import PlaylistInfoModal from './components/PlaylistInfo.svelte' 10 10 import { queue_visible } from './lib/queue' 11 11 import { ipc_listen, ipc_renderer } from '@/lib/window' 12 - import { delete_track_list, get_track_list, import_tracks, type PlaylistInfo } from '@/lib/data' 12 + import { 13 + delete_track_list, 14 + get_track_list, 15 + import_tracks, 16 + view_options, 17 + type PlaylistInfo, 18 + } from '@/lib/data' 13 19 import { play_pause } from './lib/player' 14 20 import DragGhost from './components/DragGhost.svelte' 15 21 import ItunesImport from './components/ItunesImport.svelte' ··· 18 24 import { check_shortcut } from './lib/helpers' 19 25 import ArtistList from './components/ArtistList.svelte' 20 26 import { tracklist_actions } from './lib/page' 21 - import './lib/router' 22 27 import Route from './lib/Route.svelte' 23 28 import { navigate_back, navigate_forward } from './lib/router' 29 + import './lib/router' 24 30 25 31 ipc_renderer.invoke('app_loaded').catch(() => { 26 32 ipc_renderer.invoke('showMessageBox', false, { ··· 29 35 detail: 'Graceful shutdown will not be possible.', 30 36 }) 31 37 }) 38 + 39 + if (window.navigator.onLine) { 40 + ipc_renderer.invoke('check_for_updates') 41 + } 32 42 33 43 async function open_import_dialog() { 34 44 if ($modal_count !== 0) {
+5
src/electron/ipc.ts
··· 2 2 import { ipc_main } from './typed_ipc' 3 3 import path from 'path' 4 4 import is from './is' 5 + import { check_for_updates } from './update' 6 + 7 + ipc_main.handle('check_for_updates', async (_e) => { 8 + check_for_updates() 9 + }) 5 10 6 11 ipc_main.handle('showMessageBox', async (e, attached, options) => { 7 12 const window = BrowserWindow.fromWebContents(e.sender)
+7 -5
src/electron/typed_ipc.ts
··· 11 11 import { ipcMain as electronIpcMain } from 'electron' 12 12 import type { TrackID } from '../../ferrum-addon' 13 13 14 - type OptionalPromise<T> = T | Promise<T> 15 14 type InputMap = { 16 15 // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 16 [key: string]: (...args: any[]) => any ··· 45 44 listener: ( 46 45 event: TypedIpcMainInvokeEvent<IpcEvents>, 47 46 ...args: Parameters<IpcCommands[K]> 48 - ) => OptionalPromise<ReturnType<IpcCommands[K]>>, 47 + ) => ReturnType<IpcCommands[K]>, 49 48 ): void 50 49 handleOnce<K extends keyof IpcCommands>( 51 50 channel: K, 52 51 listener: ( 53 52 event: TypedIpcMainInvokeEvent<IpcEvents>, 54 53 ...args: Parameters<IpcCommands[K]> 55 - ) => OptionalPromise<ReturnType<IpcCommands[K]>>, 54 + ) => ReturnType<IpcCommands[K]>, 56 55 ): void 57 56 removeHandler<K extends keyof IpcCommands>(channel: K): void 58 57 } ··· 140 139 queue: boolean 141 140 } 142 141 142 + type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } 143 + 143 144 type Commands = { 144 145 app_loaded: () => void 145 146 showMessageBox: ( ··· 150 151 attached: boolean, 151 152 options: Parameters<typeof dialog.showOpenDialog>[0], 152 153 ) => ReturnType<typeof dialog.showOpenDialog> 154 + check_for_updates: () => void 153 155 revealTrackFile: (...paths: string[]) => void 154 - show_tracks_menu: (options: ShowTrackMenuOptions) => null | SelectedTracksAction 156 + show_tracks_menu: (options: ShowTrackMenuOptions) => Promise<null | SelectedTracksAction> 155 157 showTracklistMenu: (options: { id: string; isFolder: boolean; isRoot: boolean }) => void 156 - show_columns_menu: (options: { menu: Electron.MenuItemConstructorOptions[] }) => void 158 + show_columns_menu: (options: { menu: MenuItemConstructorOptions[] }) => void 157 159 volume_change: (up: boolean) => void 158 160 159 161 'update:Shuffle': (checked: boolean) => void
+92
src/electron/update.ts
··· 1 + import { app, dialog, shell } from 'electron' 2 + import package_json from '../../package.json' 3 + 4 + export type UpdateJson = { 5 + [channel: string]: { 6 + url: string 7 + version: string 8 + } 9 + } 10 + 11 + export type JsonValue = 12 + | string 13 + | number 14 + | boolean 15 + | null 16 + | JsonValue[] 17 + | { [key: string]: JsonValue } 18 + 19 + function popup(message: string, detail: string) { 20 + console.error(`${message}: ${detail}`) 21 + dialog.showMessageBox({ 22 + type: 'error', 23 + message, 24 + detail, 25 + }) 26 + } 27 + 28 + async function fetch_json(url: string) { 29 + const response = await fetch(url, {}).catch((error: Error) => error) 30 + 31 + if (response instanceof Error) { 32 + return { error: response.message, data: null } 33 + } else if (response.status !== 200) { 34 + return { error: `${response.status}: ${response.statusText}`, data: null } 35 + } 36 + 37 + const value: JsonValue = await response.json().catch(() => { 38 + return null 39 + }) 40 + if (value === null) { 41 + // also handle JSON null value as error 42 + return { error: 'Could not parse JSON', data: null } 43 + } 44 + return { data: value, error: null } 45 + } 46 + 47 + export async function check_for_updates() { 48 + const release_result = await fetch_json(package_json['latest-release-api-url']) 49 + if (release_result.error) { 50 + return popup('Failed to check for updates', release_result.error) 51 + } 52 + const release = release_result.data 53 + 54 + if ( 55 + !release || 56 + typeof release !== 'object' || 57 + !('name' in release) || 58 + typeof release.name !== 'string' 59 + ) { 60 + return popup('Failed to check for updates', 'Could not parse JSON') 61 + } 62 + const update_json_url = `${package_json.repository}/releases/download/${release.name}/update.json` 63 + 64 + const update_result = await fetch_json(update_json_url) 65 + if (update_result.error) { 66 + return popup('Failed to check for updates', update_result.error) 67 + } 68 + const update_json = update_result.data as UpdateJson 69 + console.log(update_json) 70 + 71 + const channel = update_json['stable-0'] 72 + if (!channel) { 73 + return popup('Failed to check for updates', 'Could not find update channel') 74 + } 75 + 76 + const app_version = app.getVersion() 77 + 78 + if (channel.version === app_version) { 79 + return 80 + } 81 + 82 + const result = await dialog.showMessageBox({ 83 + type: 'info', 84 + message: 'A new version of Ferrum is available!', 85 + detail: `Ferrum ${channel.version} is available. You are currently on ${app_version}`, 86 + buttons: ['Update', 'Cancel'], 87 + defaultId: 0, 88 + }) 89 + if (result.response === 0) { 90 + await shell.openExternal(channel.url) 91 + } 92 + }