mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-20 12:15:26 -07:00
fix(launcher): restore Matroska thumbnails in Linux rofi picker (#210)
This commit is contained in:
@@ -274,7 +274,8 @@ jobs:
|
|||||||
config.example.jsonc \
|
config.example.jsonc \
|
||||||
plugin/subminer \
|
plugin/subminer \
|
||||||
plugin/subminer.conf \
|
plugin/subminer.conf \
|
||||||
assets/themes/subminer.rasi
|
assets/themes/subminer.rasi \
|
||||||
|
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer
|
||||||
|
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -273,7 +273,8 @@ jobs:
|
|||||||
config.example.jsonc \
|
config.example.jsonc \
|
||||||
plugin/subminer \
|
plugin/subminer \
|
||||||
plugin/subminer.conf \
|
plugin/subminer.conf \
|
||||||
assets/themes/subminer.rasi
|
assets/themes/subminer.rasi \
|
||||||
|
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer
|
||||||
|
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
APP_NAME := subminer
|
APP_NAME := subminer
|
||||||
THEME_SOURCE := assets/themes/subminer.rasi
|
THEME_SOURCE := assets/themes/subminer.rasi
|
||||||
|
THUMBNAILER_SOURCE := assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer
|
||||||
LAUNCHER_OUT := dist/launcher/$(APP_NAME)
|
LAUNCHER_OUT := dist/launcher/$(APP_NAME)
|
||||||
THEME_FILE := subminer.rasi
|
THEME_FILE := subminer.rasi
|
||||||
|
THUMBNAILER_FILE := subminer-ffmpegthumbnailer.thumbnailer
|
||||||
|
|
||||||
# Default install prefix for the wrapper script.
|
# Default install prefix for the wrapper script.
|
||||||
PREFIX ?= $(HOME)/.local
|
PREFIX ?= $(HOME)/.local
|
||||||
@@ -221,11 +223,13 @@ docs-dev: ensure-bun
|
|||||||
|
|
||||||
|
|
||||||
install-linux: build-launcher
|
install-linux: build-launcher
|
||||||
@printf '%s\n' "[INFO] Installing Linux wrapper/theme artifacts"
|
@printf '%s\n' "[INFO] Installing Linux wrapper/support artifacts"
|
||||||
@install -d "$(BINDIR)"
|
@install -d "$(BINDIR)"
|
||||||
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
||||||
@install -d "$(LINUX_DATA_DIR)/themes"
|
@install -d "$(LINUX_DATA_DIR)/themes"
|
||||||
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
@install -d "$(LINUX_DATA_DIR)/thumbnailers"
|
||||||
|
@install -m 0644 "./$(THUMBNAILER_SOURCE)" "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
@install -d "$(LINUX_DATA_DIR)/plugin/subminer"
|
@install -d "$(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
@cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/"
|
@cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/"
|
||||||
@if [ -n "$(APPIMAGE_SRC)" ]; then \
|
@if [ -n "$(APPIMAGE_SRC)" ]; then \
|
||||||
@@ -234,7 +238,7 @@ install-linux: build-launcher
|
|||||||
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
||||||
printf '%s\n' " Build one with: make build"; \
|
printf '%s\n' " Build one with: make build"; \
|
||||||
fi
|
fi
|
||||||
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
|
|
||||||
install-macos: build-launcher
|
install-macos: build-launcher
|
||||||
@printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts"
|
@printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts"
|
||||||
@@ -275,8 +279,9 @@ uninstall:
|
|||||||
uninstall-linux:
|
uninstall-linux:
|
||||||
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||||
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
@rm -f "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
@rm -rf "$(LINUX_DATA_DIR)/plugin/subminer"
|
@rm -rf "$(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer"
|
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
|
|
||||||
uninstall-macos:
|
uninstall-macos:
|
||||||
@rm -f "$(BINDIR)/subminer"
|
@rm -f "$(BINDIR)/subminer"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[Thumbnailer Entry]
|
||||||
|
TryExec=ffmpegthumbnailer
|
||||||
|
Exec=ffmpegthumbnailer -i %i -o %o -s %s -f
|
||||||
|
MimeType=video/matroska;video/matroska-3d;video/x-matroska;video/x-matroska-3d;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: launcher
|
||||||
|
|
||||||
|
- Fixed missing MKV thumbnails in the Linux rofi picker when system thumbnailer registrations only advertise legacy Matroska MIME aliases.
|
||||||
@@ -392,7 +392,7 @@ subminer -u
|
|||||||
subminer --update
|
subminer --update
|
||||||
```
|
```
|
||||||
|
|
||||||
SubMiner verifies AppImage, launcher, and Linux support-asset downloads against `SHA256SUMS.txt`. On Linux those support assets include the launcher-managed runtime plugin copy under `SubMiner/plugin/subminer` plus the rofi theme at `SubMiner/themes/subminer.rasi`. If the binary is in a protected path, SubMiner shows the exact command to run rather than elevating itself.
|
SubMiner verifies AppImage, launcher, and Linux support-asset downloads against `SHA256SUMS.txt`. On Linux those support assets include the launcher-managed runtime plugin copy under `SubMiner/plugin/subminer`, the rofi theme at `SubMiner/themes/subminer.rasi`, and the scoped Matroska thumbnailer registration under `SubMiner/thumbnailers`. If the binary is in a protected path, SubMiner shows the exact command to run rather than elevating itself.
|
||||||
|
|
||||||
The tray "Check for Updates" entry installs the new app automatically on Linux, macOS, and Windows. On Linux it replaces the running `.AppImage` in place via `electron-updater` and refreshes the managed support assets from `subminer-assets.tar.gz`; AppImages managed by a system package (for example the AUR `/opt/SubMiner/SubMiner.AppImage`) are skipped so the package manager stays in charge.
|
The tray "Check for Updates" entry installs the new app automatically on Linux, macOS, and Windows. On Linux it replaces the running `.AppImage` in place via `electron-updater` and refreshes the managed support assets from `subminer-assets.tar.gz`; AppImages managed by a system package (for example the AUR `/opt/SubMiner/SubMiner.AppImage`) are skipped so the package manager stays in charge.
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ SubMiner is an overlay that sits on top of mpv. It connects to mpv through an IP
|
|||||||
|
|
||||||
The `subminer` launcher handles mpv IPC socket setup automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or `\\.\pipe\subminer-socket` on Windows) - without it the overlay starts but subtitles won't appear.
|
The `subminer` launcher handles mpv IPC socket setup automatically. If you launch mpv yourself or from another tool, you must pass `--input-ipc-server=/tmp/subminer-socket` (or `\\.\pipe\subminer-socket` on Windows) - without it the overlay starts but subtitles won't appear.
|
||||||
|
|
||||||
The bundled mpv plugin is injected at runtime automatically - you don't need to install it separately. On Linux, the `subminer` launcher now checks for its managed runtime plugin copy and rofi theme before every mpv-managed launch and installs those support assets from the bundled app automatically if either one is missing. It provides in-player keybindings (the `y` chord) for controlling the overlay from within mpv. See [MPV Plugin](/mpv-plugin) for the full keybinding and configuration reference.
|
The bundled mpv plugin is injected at runtime automatically - you don't need to install it separately. On Linux, the `subminer` launcher checks for its managed runtime plugin copy, rofi theme, and scoped thumbnailer registration before every mpv-managed launch and installs those support assets from the bundled app automatically if one is missing. For a rofi picker launch, this check runs before the picker opens. It provides in-player keybindings (the `y` chord) for controlling the overlay from within mpv. See [MPV Plugin](/mpv-plugin) for the full keybinding and configuration reference.
|
||||||
|
|
||||||
## Platform Notes
|
## Platform Notes
|
||||||
|
|
||||||
@@ -456,18 +456,20 @@ sudo chmod +x /usr/local/bin/subminer
|
|||||||
|
|
||||||
### Linux Support Assets
|
### Linux Support Assets
|
||||||
|
|
||||||
SubMiner ships the Linux rofi theme plus the launcher-managed runtime plugin copy in `subminer-assets.tar.gz`:
|
SubMiner ships the Linux rofi theme, scoped Matroska thumbnailer registration, and launcher-managed runtime plugin copy in `subminer-assets.tar.gz`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
||||||
mkdir -p ~/.local/share/SubMiner/themes
|
mkdir -p ~/.local/share/SubMiner/themes
|
||||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
||||||
|
mkdir -p ~/.local/share/SubMiner/thumbnailers
|
||||||
|
cp /tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer ~/.local/share/SubMiner/thumbnailers/
|
||||||
mkdir -p ~/.local/share/SubMiner/plugin
|
mkdir -p ~/.local/share/SubMiner/plugin
|
||||||
cp -R /tmp/plugin/subminer ~/.local/share/SubMiner/plugin/subminer
|
cp -R /tmp/plugin/subminer ~/.local/share/SubMiner/plugin/subminer
|
||||||
```
|
```
|
||||||
|
|
||||||
`subminer -u` and the tray updater keep those Linux support assets in sync automatically once the `SubMiner` data dir exists. Normal Linux launcher playback also auto-installs the managed runtime plugin copy and rofi theme from the bundled app if either support asset is missing, so manual extraction is mainly useful for pre-seeding or custom setups.
|
`subminer -u` and the tray updater keep those Linux support assets in sync automatically once the `SubMiner` data dir exists. Normal Linux launcher playback also auto-installs all three assets from the bundled app if one is missing, so manual extraction is mainly useful for pre-seeding or custom setups. Rofi receives the SubMiner data path through its process-local `XDG_DATA_DIRS`, so the thumbnailer registration does not change the desktop-wide configuration.
|
||||||
|
|
||||||
Override the theme path with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
|
Override the theme path with `SUBMINER_ROFI_THEME=/absolute/path/to/theme.rasi`.
|
||||||
|
|
||||||
|
|||||||
@@ -34,18 +34,22 @@ subminer -R -r -d ~/Anime # rofi picker, recursive
|
|||||||
subminer -R /directory # rofi picker, directory shortcut
|
subminer -R /directory # rofi picker, directory shortcut
|
||||||
```
|
```
|
||||||
|
|
||||||
rofi shows a GUI menu with icon thumbnails when available. SubMiner ships the rofi theme plus the Linux launcher-managed runtime plugin copy in the release assets tarball:
|
rofi shows a GUI menu with icon thumbnails when available. SubMiner ships the rofi theme, a scoped `ffmpegthumbnailer` MIME registration, and the Linux launcher-managed runtime plugin copy in the release assets tarball:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
||||||
mkdir -p ~/.local/share/SubMiner/themes
|
mkdir -p ~/.local/share/SubMiner/themes
|
||||||
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
cp /tmp/assets/themes/subminer.rasi ~/.local/share/SubMiner/themes/subminer.rasi
|
||||||
|
mkdir -p ~/.local/share/SubMiner/thumbnailers
|
||||||
|
cp /tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer ~/.local/share/SubMiner/thumbnailers/
|
||||||
mkdir -p ~/.local/share/SubMiner/plugin
|
mkdir -p ~/.local/share/SubMiner/plugin
|
||||||
cp -R /tmp/plugin/subminer ~/.local/share/SubMiner/plugin/subminer
|
cp -R /tmp/plugin/subminer ~/.local/share/SubMiner/plugin/subminer
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the `SubMiner` data dir exists, `subminer -u` refreshes both assets automatically. Normal Linux launcher playback also checks for the managed runtime plugin copy and rofi theme before mpv launch and installs them from the bundled app automatically if either one is missing.
|
Once the `SubMiner` data dir exists, `subminer -u` refreshes these assets automatically. Normal Linux launcher playback checks for all three assets and installs them from the bundled app when one is missing. For `subminer -R`, this repair runs before rofi opens.
|
||||||
|
|
||||||
|
When `ffmpegthumbnailer` is installed, SubMiner prepends its own data directory to `XDG_DATA_DIRS` for the rofi process only. This lets rofi recognize the canonical Matroska MIME types used by newer GLib versions without changing the desktop-wide MIME or thumbnailer configuration. An existing registration in your own `$XDG_DATA_HOME/thumbnailers` still takes priority.
|
||||||
|
|
||||||
The theme is auto-detected from these paths (first match wins):
|
The theme is auto-detected from these paths (first match wins):
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type HistorySeriesEntry,
|
type HistorySeriesEntry,
|
||||||
} from '../history.js';
|
} from '../history.js';
|
||||||
import type { Args } from '../types.js';
|
import type { Args } from '../types.js';
|
||||||
|
import { ensureLinuxRuntimePluginAvailable } from '../runtime-plugin-preflight.js';
|
||||||
import type { LauncherCommandContext } from './context.js';
|
import type { LauncherCommandContext } from './context.js';
|
||||||
|
|
||||||
export type HistorySessionAction = 'previous' | 'replay' | 'next' | 'browse' | 'quit';
|
export type HistorySessionAction = 'previous' | 'replay' | 'next' | 'browse' | 'quit';
|
||||||
@@ -333,6 +334,13 @@ export async function runHistoryCommand(
|
|||||||
const { args, scriptPath } = context;
|
const { args, scriptPath } = context;
|
||||||
|
|
||||||
checkPickerDependencies(args);
|
checkPickerDependencies(args);
|
||||||
|
if (args.useRofi) {
|
||||||
|
await ensureLinuxRuntimePluginAvailable({
|
||||||
|
appPath: context.appPath ?? undefined,
|
||||||
|
scriptPath,
|
||||||
|
logLevel: args.logLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
const themePath = args.useRofi ? findRofiTheme(scriptPath) : null;
|
const themePath = args.useRofi ? findRofiTheme(scriptPath) : null;
|
||||||
|
|
||||||
const dbPath = resolveImmersionDbPath();
|
const dbPath = resolveImmersionDbPath();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { fail } from '../log.js';
|
|||||||
import { runAppCommandWithInherit } from '../mpv.js';
|
import { runAppCommandWithInherit } from '../mpv.js';
|
||||||
import { commandExists } from '../util.js';
|
import { commandExists } from '../util.js';
|
||||||
import { runJellyfinPlayMenu } from '../jellyfin.js';
|
import { runJellyfinPlayMenu } from '../jellyfin.js';
|
||||||
|
import { ensureLinuxRuntimePluginAvailable } from '../runtime-plugin-preflight.js';
|
||||||
import { shouldForwardLogLevel } from '../types.js';
|
import { shouldForwardLogLevel } from '../types.js';
|
||||||
import type { LauncherCommandContext } from './context.js';
|
import type { LauncherCommandContext } from './context.js';
|
||||||
|
|
||||||
@@ -64,6 +65,13 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
|
|||||||
if (args.useRofi && !commandExists('rofi')) {
|
if (args.useRofi && !commandExists('rofi')) {
|
||||||
fail('rofi not found. Install rofi or omit -R for fzf.');
|
fail('rofi not found. Install rofi or omit -R for fzf.');
|
||||||
}
|
}
|
||||||
|
if (args.useRofi) {
|
||||||
|
await ensureLinuxRuntimePluginAvailable({
|
||||||
|
appPath,
|
||||||
|
scriptPath,
|
||||||
|
logLevel: args.logLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
await runJellyfinPlayMenu(appPath, args, scriptPath, mpvSocketPath);
|
await runJellyfinPlayMenu(appPath, args, scriptPath, mpvSocketPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -496,3 +496,39 @@ test('playback command ensures Linux runtime plugin before mpv launch', async ()
|
|||||||
|
|
||||||
assert.deepEqual(calls, ['plugin', 'startMpv']);
|
assert.deepEqual(calls, ['plugin', 'startMpv']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rofi playback repairs support assets before opening the picker', async () => {
|
||||||
|
const context = createContext();
|
||||||
|
context.args = {
|
||||||
|
...context.args,
|
||||||
|
target: '',
|
||||||
|
targetKind: '',
|
||||||
|
useRofi: true,
|
||||||
|
};
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
await runPlaybackCommandWithDeps(context, {
|
||||||
|
ensurePlaybackSetupReady: async () => {},
|
||||||
|
ensureRuntimePluginReady: async () => {
|
||||||
|
calls.push('assets');
|
||||||
|
},
|
||||||
|
chooseTarget: async () => {
|
||||||
|
calls.push('picker');
|
||||||
|
return { target: '/tmp/movie.mkv', kind: 'file' };
|
||||||
|
},
|
||||||
|
checkPickerDependencies: () => {},
|
||||||
|
checkDependencies: () => {},
|
||||||
|
registerCleanup: () => {},
|
||||||
|
startMpv: async () => {
|
||||||
|
calls.push('startMpv');
|
||||||
|
},
|
||||||
|
waitForUnixSocketReady: async () => true,
|
||||||
|
startOverlay: async () => {},
|
||||||
|
launchAppCommandDetached: () => {},
|
||||||
|
log: () => {},
|
||||||
|
cleanupPlaybackSession: async () => {},
|
||||||
|
getMpvProc: () => null,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['assets', 'picker', 'startMpv']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export async function runPlaybackCommand(context: LauncherCommandContext): Promi
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
chooseTarget,
|
chooseTarget,
|
||||||
|
checkPickerDependencies,
|
||||||
checkDependencies,
|
checkDependencies,
|
||||||
registerCleanup,
|
registerCleanup,
|
||||||
startMpv,
|
startMpv,
|
||||||
@@ -177,6 +178,7 @@ type PlaybackCommandDeps = {
|
|||||||
args: Args,
|
args: Args,
|
||||||
scriptPath: string,
|
scriptPath: string,
|
||||||
) => Promise<{ target: string; kind: 'file' | 'url' } | null>;
|
) => Promise<{ target: string; kind: 'file' | 'url' } | null>;
|
||||||
|
checkPickerDependencies?: (args: Args) => void;
|
||||||
checkDependencies: (args: Args) => void;
|
checkDependencies: (args: Args) => void;
|
||||||
registerCleanup: (context: LauncherCommandContext) => void;
|
registerCleanup: (context: LauncherCommandContext) => void;
|
||||||
startMpv: typeof startMpv;
|
startMpv: typeof startMpv;
|
||||||
@@ -201,7 +203,18 @@ export async function runPlaybackCommandWithDeps(
|
|||||||
await deps.ensurePlaybackSetupReady(context);
|
await deps.ensurePlaybackSetupReady(context);
|
||||||
|
|
||||||
if (!args.target) {
|
if (!args.target) {
|
||||||
checkPickerDependencies(args);
|
(deps.checkPickerDependencies ?? checkPickerDependencies)(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
let runtimeAssetsReady = false;
|
||||||
|
const ensureRuntimeAssetsReady = async (): Promise<void> => {
|
||||||
|
if (runtimeAssetsReady) return;
|
||||||
|
await deps.ensureRuntimePluginReady(context);
|
||||||
|
runtimeAssetsReady = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!args.target && args.useRofi) {
|
||||||
|
await ensureRuntimeAssetsReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetChoice = await deps.chooseTarget(args, scriptPath);
|
const targetChoice = await deps.chooseTarget(args, scriptPath);
|
||||||
@@ -266,7 +279,7 @@ export async function runPlaybackCommandWithDeps(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await deps.ensureRuntimePluginReady(context);
|
await ensureRuntimeAssetsReady();
|
||||||
|
|
||||||
await deps.startMpv(
|
await deps.startMpv(
|
||||||
selectedTarget.target,
|
selectedTarget.target,
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ test('runUpdateCommand updates directly on Linux without launching Electron', as
|
|||||||
launcher: { status: 'updated' },
|
launcher: { status: 'updated' },
|
||||||
supportAssets: [
|
supportAssets: [
|
||||||
{ status: 'updated', component: 'theme', message: 'Installed theme.' },
|
{ status: 'updated', component: 'theme', message: 'Installed theme.' },
|
||||||
|
{
|
||||||
|
status: 'updated',
|
||||||
|
component: 'thumbnailer',
|
||||||
|
message: 'Installed rofi thumbnailer.',
|
||||||
|
},
|
||||||
{ status: 'skipped', component: 'plugin', message: 'Plugin already up to date.' },
|
{ status: 'skipped', component: 'plugin', message: 'Plugin already up to date.' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -52,6 +57,7 @@ test('runUpdateCommand updates directly on Linux without launching Electron', as
|
|||||||
'info:AppImage update: updated',
|
'info:AppImage update: updated',
|
||||||
'info:Launcher update: updated',
|
'info:Launcher update: updated',
|
||||||
'info:Support assets (theme) update: updated - Installed theme.',
|
'info:Support assets (theme) update: updated - Installed theme.',
|
||||||
|
'info:Support assets (thumbnailer) update: updated - Installed rofi thumbnailer.',
|
||||||
'info:Support assets (plugin) update: skipped - Plugin already up to date.',
|
'info:Support assets (plugin) update: skipped - Plugin already up to date.',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import {
|
|||||||
parseSha256Sums,
|
parseSha256Sums,
|
||||||
type FetchLike,
|
type FetchLike,
|
||||||
} from '../../src/main/runtime/update/release-assets.js';
|
} from '../../src/main/runtime/update/release-assets.js';
|
||||||
import { updateSupportAssetsFromRelease } from '../../src/main/runtime/update/support-assets.js';
|
import {
|
||||||
|
updateSupportAssetsFromRelease,
|
||||||
|
type SupportAssetsUpdateResult,
|
||||||
|
} from '../../src/main/runtime/update/support-assets.js';
|
||||||
|
|
||||||
type UpdateCommandResponse = {
|
type UpdateCommandResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -36,15 +39,14 @@ type DirectReleaseUpdateRequest = {
|
|||||||
channel: UpdateChannel;
|
channel: UpdateChannel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DirectSupportAssetsUpdateResult = Omit<SupportAssetsUpdateResult, 'status'> & {
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
type DirectReleaseUpdateResult = {
|
type DirectReleaseUpdateResult = {
|
||||||
appImage: { status: string; command?: string; message?: string };
|
appImage: { status: string; command?: string; message?: string };
|
||||||
launcher: { status: string; command?: string; message?: string };
|
launcher: { status: string; command?: string; message?: string };
|
||||||
supportAssets: Array<{
|
supportAssets: DirectSupportAssetsUpdateResult[];
|
||||||
status: string;
|
|
||||||
component?: 'theme' | 'plugin';
|
|
||||||
command?: string;
|
|
||||||
message?: string;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpdateCommandDeps = {
|
type UpdateCommandDeps = {
|
||||||
@@ -129,12 +131,7 @@ function readUpdateChannel(root: Record<string, unknown> | null): UpdateChannel
|
|||||||
|
|
||||||
function logUpdateResult(
|
function logUpdateResult(
|
||||||
label: string,
|
label: string,
|
||||||
result: {
|
result: DirectSupportAssetsUpdateResult,
|
||||||
status: string;
|
|
||||||
component?: 'theme' | 'plugin';
|
|
||||||
command?: string;
|
|
||||||
message?: string;
|
|
||||||
},
|
|
||||||
configuredLogLevel: NonNullable<LauncherCommandContext['args']['logLevel']>,
|
configuredLogLevel: NonNullable<LauncherCommandContext['args']['logLevel']>,
|
||||||
deps: Pick<UpdateCommandDeps, 'log'>,
|
deps: Pick<UpdateCommandDeps, 'log'>,
|
||||||
): void {
|
): void {
|
||||||
|
|||||||
@@ -73,20 +73,21 @@ function makeTestEnv(homeDir: string, xdgConfigHome: string): NodeJS.ProcessEnv
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Linux the playback path runs `ensureLinuxRuntimePluginAvailable`, which —
|
// On Linux the playback path runs `ensureLinuxRuntimePluginAvailable`, which
|
||||||
// when the runtime plugin/theme are missing — spawns the app with
|
// spawns the app with `--ensure-linux-runtime-plugin-assets` when managed
|
||||||
// `--ensure-linux-runtime-plugin-assets` and polls up to 30s
|
// support assets are missing and polls up to 30s
|
||||||
// (RESPONSE_TIMEOUT_MS) for an install response. A fake app that just exits
|
// (RESPONSE_TIMEOUT_MS) for an install response. A fake app that just exits
|
||||||
// never writes that response, so the launcher hangs and the test times out on
|
// never writes that response, so the launcher hangs and the test times out on
|
||||||
// Linux CI (the preflight is a no-op on macOS/Windows). This shell prelude makes
|
// Linux CI (the preflight is a no-op on macOS/Windows). This shell prelude makes
|
||||||
// the fake app install the managed plugin/theme and write the response, matching
|
// the fake app install the managed support assets and write the response, matching
|
||||||
// launcher/smoke.e2e.test.ts. Prepend it to each fake app that reaches playback.
|
// launcher/smoke.e2e.test.ts. Prepend it to each fake app that reaches playback.
|
||||||
const RUNTIME_PLUGIN_PREFLIGHT_SH = `if [ "$1" = "--ensure-linux-runtime-plugin-assets" ]; then
|
const RUNTIME_PLUGIN_PREFLIGHT_SH = `if [ "$1" = "--ensure-linux-runtime-plugin-assets" ]; then
|
||||||
data="\${XDG_DATA_HOME:-$HOME/.local/share}/SubMiner"
|
data="\${XDG_DATA_HOME:-$HOME/.local/share}/SubMiner"
|
||||||
mkdir -p "$data/plugin/subminer" "$data/themes"
|
mkdir -p "$data/plugin/subminer" "$data/themes" "$data/thumbnailers"
|
||||||
printf -- '-- test plugin\\n' > "$data/plugin/subminer/main.lua"
|
printf -- '-- test plugin\\n' > "$data/plugin/subminer/main.lua"
|
||||||
printf 'test=true\\n' > "$data/plugin/subminer.conf"
|
printf 'test=true\\n' > "$data/plugin/subminer.conf"
|
||||||
printf '/* test theme */\\n' > "$data/themes/subminer.rasi"
|
printf '/* test theme */\\n' > "$data/themes/subminer.rasi"
|
||||||
|
printf '[Thumbnailer Entry]\\n' > "$data/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer"
|
||||||
if [ "$2" = "--ensure-linux-runtime-plugin-assets-response-path" ] && [ -n "$3" ]; then
|
if [ "$2" = "--ensure-linux-runtime-plugin-assets-response-path" ] && [ -n "$3" ]; then
|
||||||
mkdir -p "$(dirname "$3")"
|
mkdir -p "$(dirname "$3")"
|
||||||
printf '{"ok":true,"status":"installed","path":"%s"}' "$data/plugin/subminer/main.lua" > "$3"
|
printf '{"ok":true,"status":"installed","path":"%s"}' "$data/plugin/subminer/main.lua" > "$3"
|
||||||
|
|||||||
+46
-1
@@ -3,7 +3,12 @@ import assert from 'node:assert/strict';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { findRofiTheme, formatRofiPrompt } from './picker';
|
import {
|
||||||
|
findRofiTheme,
|
||||||
|
findRofiThumbnailerDataRoot,
|
||||||
|
formatRofiPrompt,
|
||||||
|
prependXdgDataDir,
|
||||||
|
} from './picker';
|
||||||
|
|
||||||
// ── formatRofiPrompt: spacing between prompt and input field ──────────────────
|
// ── formatRofiPrompt: spacing between prompt and input field ──────────────────
|
||||||
|
|
||||||
@@ -23,6 +28,7 @@ test('formatRofiPrompt leaves an empty prompt empty', () => {
|
|||||||
// ── findRofiTheme: Linux packaged path discovery ──────────────────────────────
|
// ── findRofiTheme: Linux packaged path discovery ──────────────────────────────
|
||||||
|
|
||||||
const ROFI_THEME_FILE = 'subminer.rasi';
|
const ROFI_THEME_FILE = 'subminer.rasi';
|
||||||
|
const ROFI_THUMBNAILER_FILE = 'subminer-ffmpegthumbnailer.thumbnailer';
|
||||||
|
|
||||||
function makeFile(filePath: string): void {
|
function makeFile(filePath: string): void {
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
@@ -121,3 +127,42 @@ test('findRofiTheme resolves ~/.local/share/SubMiner/themes/subminer.rasi when X
|
|||||||
fs.rmSync(baseDir, { recursive: true, force: true });
|
fs.rmSync(baseDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('findRofiThumbnailerDataRoot resolves the managed XDG data root', () => {
|
||||||
|
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-xdg-'));
|
||||||
|
const originalXdgDataHome = process.env.XDG_DATA_HOME;
|
||||||
|
try {
|
||||||
|
process.env.XDG_DATA_HOME = xdgDataHome;
|
||||||
|
const dataRoot = path.join(xdgDataHome, 'SubMiner');
|
||||||
|
makeFile(path.join(dataRoot, 'thumbnailers', ROFI_THUMBNAILER_FILE));
|
||||||
|
|
||||||
|
const result = withPlatform('linux', () => findRofiThumbnailerDataRoot('/usr/bin/subminer'));
|
||||||
|
assert.equal(result, dataRoot);
|
||||||
|
} finally {
|
||||||
|
if (originalXdgDataHome === undefined) {
|
||||||
|
delete process.env.XDG_DATA_HOME;
|
||||||
|
} else {
|
||||||
|
process.env.XDG_DATA_HOME = originalXdgDataHome;
|
||||||
|
}
|
||||||
|
fs.rmSync(xdgDataHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('findRofiThumbnailerDataRoot is Linux-only', () => {
|
||||||
|
assert.equal(
|
||||||
|
withPlatform('darwin', () => findRofiThumbnailerDataRoot('/usr/bin/subminer')),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prependXdgDataDir preserves existing roots and avoids duplicates', () => {
|
||||||
|
const root = '/tmp/subminer-data';
|
||||||
|
assert.equal(
|
||||||
|
prependXdgDataDir(root, `/opt/share${path.delimiter}${root}${path.delimiter}/usr/share`),
|
||||||
|
`${root}${path.delimiter}/opt/share${path.delimiter}/usr/share`,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
prependXdgDataDir(root),
|
||||||
|
`${root}${path.delimiter}/usr/local/share${path.delimiter}/usr/share`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ interface RofiIconEntry {
|
|||||||
iconPath?: string;
|
iconPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROFI_THUMBNAILER_FILE = 'subminer-ffmpegthumbnailer.thumbnailer';
|
||||||
|
const DEFAULT_XDG_DATA_DIRS = ['/usr/local/share', '/usr/share'];
|
||||||
|
|
||||||
function showRofiIconMenu(
|
function showRofiIconMenu(
|
||||||
entries: RofiIconEntry[],
|
entries: RofiIconEntry[],
|
||||||
prompt: string,
|
prompt: string,
|
||||||
@@ -389,6 +392,47 @@ export function findRofiTheme(scriptPath: string): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findRofiThumbnailerDataRoot(scriptPath: string): string | null {
|
||||||
|
if (process.platform !== 'linux') return null;
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(realpathMaybe(scriptPath));
|
||||||
|
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share');
|
||||||
|
const roots = [
|
||||||
|
path.join(xdgDataHome, 'SubMiner'),
|
||||||
|
path.posix.join('/usr/local/share/SubMiner'),
|
||||||
|
path.posix.join('/usr/share/SubMiner'),
|
||||||
|
path.join(scriptDir, 'assets'),
|
||||||
|
path.join(scriptDir, '..', 'assets'),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const root of roots) {
|
||||||
|
if (fs.existsSync(path.join(root, 'thumbnailers', ROFI_THUMBNAILER_FILE))) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prependXdgDataDir(dataRoot: string, currentValue?: string): string {
|
||||||
|
const currentDirs = currentValue
|
||||||
|
? currentValue.split(path.delimiter).filter(Boolean)
|
||||||
|
: DEFAULT_XDG_DATA_DIRS;
|
||||||
|
return [dataRoot, ...currentDirs.filter((candidate) => candidate !== dataRoot)].join(
|
||||||
|
path.delimiter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRofiThumbnailEnvironment(scriptPath: string): NodeJS.ProcessEnv {
|
||||||
|
if (!commandExists('ffmpegthumbnailer')) return process.env;
|
||||||
|
const dataRoot = findRofiThumbnailerDataRoot(scriptPath);
|
||||||
|
if (!dataRoot) return process.env;
|
||||||
|
return {
|
||||||
|
...process.env,
|
||||||
|
XDG_DATA_DIRS: prependXdgDataDir(dataRoot, process.env.XDG_DATA_DIRS),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function showRofiMenu(
|
export function showRofiMenu(
|
||||||
videos: string[],
|
videos: string[],
|
||||||
dir: string,
|
dir: string,
|
||||||
@@ -420,6 +464,7 @@ export function showRofiMenu(
|
|||||||
const result = spawnSync('rofi', args, {
|
const result = spawnSync('rofi', args, {
|
||||||
input: buildRofiMenu(videos, dir, recursive),
|
input: buildRofiMenu(videos, dir, recursive),
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
env: buildRofiThumbnailEnvironment(scriptPath),
|
||||||
stdio: ['pipe', 'pipe', 'ignore'],
|
stdio: ['pipe', 'pipe', 'ignore'],
|
||||||
});
|
});
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
import {
|
import {
|
||||||
ensureLinuxRuntimePluginAvailable,
|
ensureLinuxRuntimePluginAvailable,
|
||||||
installManagedPluginAssetsViaApp,
|
installManagedPluginAssetsViaApp,
|
||||||
@@ -31,7 +33,7 @@ test('ensureLinuxRuntimePluginAvailable is a no-op on non-Linux platforms', asyn
|
|||||||
assert.deepEqual(calls, []);
|
assert.deepEqual(calls, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ensureLinuxRuntimePluginAvailable skips install when installed global plugin and managed theme exist', async () => {
|
test('ensureLinuxRuntimePluginAvailable skips install when plugin, theme, and thumbnailer exist', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|
||||||
await ensureLinuxRuntimePluginAvailable({
|
await ensureLinuxRuntimePluginAvailable({
|
||||||
@@ -52,13 +54,17 @@ test('ensureLinuxRuntimePluginAvailable skips install when installed global plug
|
|||||||
calls.push('theme');
|
calls.push('theme');
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
isManagedThumbnailerAvailable: () => {
|
||||||
|
calls.push('thumbnailer');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
log: () => {},
|
log: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(calls, ['detect', 'theme']);
|
assert.deepEqual(calls, ['detect', 'theme', 'thumbnailer']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ensureLinuxRuntimePluginAvailable skips install when managed runtime path and theme already resolve', async () => {
|
test('ensureLinuxRuntimePluginAvailable skips install when all managed assets resolve', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|
||||||
await ensureLinuxRuntimePluginAvailable({
|
await ensureLinuxRuntimePluginAvailable({
|
||||||
@@ -80,14 +86,19 @@ test('ensureLinuxRuntimePluginAvailable skips install when managed runtime path
|
|||||||
calls.push('theme');
|
calls.push('theme');
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
isManagedThumbnailerAvailable: () => {
|
||||||
|
calls.push('thumbnailer');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
log: () => {},
|
log: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(calls, ['detect', 'resolve', 'theme']);
|
assert.deepEqual(calls, ['detect', 'resolve', 'theme', 'thumbnailer']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ensureLinuxRuntimePluginAvailable installs managed assets when rofi theme is missing', async () => {
|
test('ensureLinuxRuntimePluginAvailable installs managed assets when rofi theme is missing', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
let themeAvailable = false;
|
||||||
|
|
||||||
await ensureLinuxRuntimePluginAvailable({
|
await ensureLinuxRuntimePluginAvailable({
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
@@ -102,10 +113,15 @@ test('ensureLinuxRuntimePluginAvailable installs managed assets when rofi theme
|
|||||||
},
|
},
|
||||||
isManagedThemeAvailable: () => {
|
isManagedThemeAvailable: () => {
|
||||||
calls.push('theme');
|
calls.push('theme');
|
||||||
return false;
|
return themeAvailable;
|
||||||
|
},
|
||||||
|
isManagedThumbnailerAvailable: () => {
|
||||||
|
calls.push('thumbnailer');
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
installManagedPluginAssets: async () => {
|
installManagedPluginAssets: async () => {
|
||||||
calls.push('install');
|
calls.push('install');
|
||||||
|
themeAvailable = true;
|
||||||
return { ok: true, status: 'installed', path: '/tmp/plugin/main.lua' };
|
return { ok: true, status: 'installed', path: '/tmp/plugin/main.lua' };
|
||||||
},
|
},
|
||||||
log: (level, _configured, message) => {
|
log: (level, _configured, message) => {
|
||||||
@@ -117,13 +133,68 @@ test('ensureLinuxRuntimePluginAvailable installs managed assets when rofi theme
|
|||||||
'detect',
|
'detect',
|
||||||
'resolve',
|
'resolve',
|
||||||
'theme',
|
'theme',
|
||||||
'info:Linux runtime support assets missing; installing managed plugin/theme assets.',
|
'info:Linux runtime support assets missing; installing managed plugin/theme/thumbnailer assets.',
|
||||||
'install',
|
'install',
|
||||||
'info:Managed Linux runtime support assets installed: plugin=/tmp/plugin/main.lua theme=/tmp/xdg-data/SubMiner/themes/subminer.rasi',
|
'info:Managed Linux runtime support assets installed: plugin=/tmp/plugin/main.lua theme=/tmp/xdg-data/SubMiner/themes/subminer.rasi thumbnailer=/tmp/xdg-data/SubMiner/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
'resolve',
|
'resolve',
|
||||||
|
'theme',
|
||||||
|
'thumbnailer',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ensureLinuxRuntimePluginAvailable installs managed assets when thumbnailer is missing', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let thumbnailerAvailable = false;
|
||||||
|
|
||||||
|
await ensureLinuxRuntimePluginAvailable({
|
||||||
|
platform: 'linux',
|
||||||
|
xdgDataHome: '/tmp/xdg-data',
|
||||||
|
detectInstalledPlugin: () => true,
|
||||||
|
resolveRuntimePluginPath: () => '/tmp/plugin/main.lua',
|
||||||
|
isManagedThemeAvailable: () => true,
|
||||||
|
isManagedThumbnailerAvailable: () => thumbnailerAvailable,
|
||||||
|
installManagedPluginAssets: async () => {
|
||||||
|
calls.push('install');
|
||||||
|
thumbnailerAvailable = true;
|
||||||
|
return { ok: true, status: 'installed', path: '/tmp/plugin/main.lua' };
|
||||||
|
},
|
||||||
|
log: (_level, _configured, message) => {
|
||||||
|
calls.push(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'Linux runtime support assets missing; installing managed plugin/theme/thumbnailer assets.',
|
||||||
|
'install',
|
||||||
|
'Managed Linux runtime support assets installed: plugin=/tmp/plugin/main.lua theme=/tmp/xdg-data/SubMiner/themes/subminer.rasi thumbnailer=/tmp/xdg-data/SubMiner/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensureLinuxRuntimePluginAvailable retains an installed plugin after installing support assets', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let thumbnailerAvailable = false;
|
||||||
|
|
||||||
|
await ensureLinuxRuntimePluginAvailable({
|
||||||
|
platform: 'linux',
|
||||||
|
xdgDataHome: '/tmp/xdg-data',
|
||||||
|
detectInstalledPlugin: () => true,
|
||||||
|
resolveRuntimePluginPath: () => {
|
||||||
|
calls.push('resolve');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
isManagedThemeAvailable: () => true,
|
||||||
|
isManagedThumbnailerAvailable: () => thumbnailerAvailable,
|
||||||
|
installManagedPluginAssets: async () => {
|
||||||
|
calls.push('install');
|
||||||
|
thumbnailerAvailable = true;
|
||||||
|
return { ok: true, status: 'installed', path: '/tmp/plugin/main.lua' };
|
||||||
|
},
|
||||||
|
log: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['install']);
|
||||||
|
});
|
||||||
|
|
||||||
test('ensureLinuxRuntimePluginAvailable installs managed assets and re-resolves plugin path', async () => {
|
test('ensureLinuxRuntimePluginAvailable installs managed assets and re-resolves plugin path', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
let resolveCount = 0;
|
let resolveCount = 0;
|
||||||
@@ -137,6 +208,8 @@ test('ensureLinuxRuntimePluginAvailable installs managed assets and re-resolves
|
|||||||
calls.push(`resolve:${resolveCount}`);
|
calls.push(`resolve:${resolveCount}`);
|
||||||
return resolveCount === 1 ? null : '/tmp/plugin/main.lua';
|
return resolveCount === 1 ? null : '/tmp/plugin/main.lua';
|
||||||
},
|
},
|
||||||
|
isManagedThemeAvailable: () => true,
|
||||||
|
isManagedThumbnailerAvailable: () => true,
|
||||||
installManagedPluginAssets: async () => {
|
installManagedPluginAssets: async () => {
|
||||||
calls.push('install');
|
calls.push('install');
|
||||||
return { ok: true, status: 'installed', path: '/tmp/plugin/main.lua' };
|
return { ok: true, status: 'installed', path: '/tmp/plugin/main.lua' };
|
||||||
@@ -148,9 +221,9 @@ test('ensureLinuxRuntimePluginAvailable installs managed assets and re-resolves
|
|||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
'resolve:1',
|
'resolve:1',
|
||||||
'info:Linux runtime support assets missing; installing managed plugin/theme assets.',
|
'info:Linux runtime support assets missing; installing managed plugin/theme/thumbnailer assets.',
|
||||||
'install',
|
'install',
|
||||||
'info:Managed Linux runtime support assets installed: plugin=/tmp/plugin/main.lua theme=/tmp/xdg-data/SubMiner/themes/subminer.rasi',
|
'info:Managed Linux runtime support assets installed: plugin=/tmp/plugin/main.lua theme=/tmp/xdg-data/SubMiner/themes/subminer.rasi thumbnailer=/tmp/xdg-data/SubMiner/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
'resolve:2',
|
'resolve:2',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -191,6 +264,60 @@ test('ensureLinuxRuntimePluginAvailable fails when runtime path remains unresolv
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ensureLinuxRuntimePluginAvailable fails when thumbnailer remains missing after install', async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
ensureLinuxRuntimePluginAvailable({
|
||||||
|
platform: 'linux',
|
||||||
|
xdgDataHome: '/tmp/xdg-data',
|
||||||
|
detectInstalledPlugin: () => true,
|
||||||
|
resolveRuntimePluginPath: () => '/tmp/plugin/main.lua',
|
||||||
|
isManagedThemeAvailable: () => true,
|
||||||
|
isManagedThumbnailerAvailable: () => false,
|
||||||
|
installManagedPluginAssets: async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 'installed',
|
||||||
|
path: '/tmp/plugin/main.lua',
|
||||||
|
}),
|
||||||
|
log: () => {},
|
||||||
|
}),
|
||||||
|
/thumbnailer=.*subminer-ffmpegthumbnailer\.thumbnailer/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensureLinuxRuntimePluginAvailable rejects a thumbnailer directory before and after install', async () => {
|
||||||
|
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-thumbnailer-directory-'));
|
||||||
|
const thumbnailerPath = path.join(
|
||||||
|
xdgDataHome,
|
||||||
|
'SubMiner',
|
||||||
|
'thumbnailers',
|
||||||
|
'subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
);
|
||||||
|
fs.mkdirSync(thumbnailerPath, { recursive: true });
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
ensureLinuxRuntimePluginAvailable({
|
||||||
|
platform: 'linux',
|
||||||
|
xdgDataHome,
|
||||||
|
detectInstalledPlugin: () => true,
|
||||||
|
isManagedThemeAvailable: () => true,
|
||||||
|
installManagedPluginAssets: async () => {
|
||||||
|
calls.push('install');
|
||||||
|
return { ok: true, status: 'installed', path: '/tmp/plugin/main.lua' };
|
||||||
|
},
|
||||||
|
log: () => {},
|
||||||
|
}),
|
||||||
|
/thumbnailer=.*subminer-ffmpegthumbnailer\.thumbnailer/i,
|
||||||
|
);
|
||||||
|
assert.deepEqual(calls, ['install']);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(xdgDataHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('installManagedPluginAssetsViaApp returns launch errors without waiting for a response file', async () => {
|
test('installManagedPluginAssetsViaApp returns launch errors without waiting for a response file', async () => {
|
||||||
let waited = false;
|
let waited = false;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type EnsureLinuxRuntimePluginAvailableOptions = {
|
|||||||
detectInstalledPlugin?: () => boolean;
|
detectInstalledPlugin?: () => boolean;
|
||||||
resolveRuntimePluginPath?: () => string | null;
|
resolveRuntimePluginPath?: () => string | null;
|
||||||
isManagedThemeAvailable?: () => boolean;
|
isManagedThemeAvailable?: () => boolean;
|
||||||
|
isManagedThumbnailerAvailable?: () => boolean;
|
||||||
installManagedPluginAssets?: () => Promise<EnsureLinuxRuntimePluginAssetsResult>;
|
installManagedPluginAssets?: () => Promise<EnsureLinuxRuntimePluginAssetsResult>;
|
||||||
log?: PreflightLog;
|
log?: PreflightLog;
|
||||||
};
|
};
|
||||||
@@ -48,6 +49,14 @@ function resolveConfiguredLogLevel(
|
|||||||
return logLevel ?? 'warn';
|
return logLevel ?? 'warn';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRegularFile(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
return fs.statSync(filePath).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForInstallResponse(
|
async function waitForInstallResponse(
|
||||||
responsePath: string,
|
responsePath: string,
|
||||||
): Promise<RuntimePluginPreflightResponse | null> {
|
): Promise<RuntimePluginPreflightResponse | null> {
|
||||||
@@ -170,15 +179,17 @@ export async function ensureLinuxRuntimePluginAvailable(
|
|||||||
});
|
});
|
||||||
const isManagedThemeAvailable =
|
const isManagedThemeAvailable =
|
||||||
options.isManagedThemeAvailable ?? (() => fs.existsSync(managedPaths.themePath));
|
options.isManagedThemeAvailable ?? (() => fs.existsSync(managedPaths.themePath));
|
||||||
|
const isManagedThumbnailerAvailable =
|
||||||
|
options.isManagedThumbnailerAvailable ?? (() => isRegularFile(managedPaths.thumbnailerPath));
|
||||||
const runtimePluginAvailable = installedPluginAvailable || Boolean(resolveRuntimePluginPath());
|
const runtimePluginAvailable = installedPluginAvailable || Boolean(resolveRuntimePluginPath());
|
||||||
if (runtimePluginAvailable && isManagedThemeAvailable()) {
|
if (runtimePluginAvailable && isManagedThemeAvailable() && isManagedThumbnailerAvailable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
'info',
|
'info',
|
||||||
configuredLogLevel,
|
configuredLogLevel,
|
||||||
'Linux runtime support assets missing; installing managed plugin/theme assets.',
|
'Linux runtime support assets missing; installing managed plugin/theme/thumbnailer assets.',
|
||||||
);
|
);
|
||||||
const installManagedPluginAssets =
|
const installManagedPluginAssets =
|
||||||
options.installManagedPluginAssets ??
|
options.installManagedPluginAssets ??
|
||||||
@@ -207,16 +218,21 @@ export async function ensureLinuxRuntimePluginAvailable(
|
|||||||
log(
|
log(
|
||||||
'info',
|
'info',
|
||||||
configuredLogLevel,
|
configuredLogLevel,
|
||||||
`Managed Linux runtime support assets installed: plugin=${installResult.path ?? 'unknown path'} theme=${managedPaths.themePath}`,
|
`Managed Linux runtime support assets installed: plugin=${installResult.path ?? 'unknown path'} theme=${managedPaths.themePath} thumbnailer=${managedPaths.thumbnailerPath}`,
|
||||||
);
|
);
|
||||||
const runtimePluginPath = resolveRuntimePluginPath();
|
const runtimePluginAvailableAfterInstall =
|
||||||
if (runtimePluginPath) {
|
installedPluginAvailable || Boolean(resolveRuntimePluginPath());
|
||||||
|
if (
|
||||||
|
runtimePluginAvailableAfterInstall &&
|
||||||
|
isManagedThemeAvailable() &&
|
||||||
|
isManagedThumbnailerAvailable()
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
`Linux managed runtime plugin assets could not be installed. ` +
|
`Linux managed runtime plugin assets could not be installed. ` +
|
||||||
`Checked path: ${managedPaths.pluginEntrypointPath}. ` +
|
`Checked paths: plugin=${managedPaths.pluginEntrypointPath} theme=${managedPaths.themePath} thumbnailer=${managedPaths.thumbnailerPath}. ` +
|
||||||
'Launch aborted before starting mpv.';
|
'Launch aborted before starting mpv.';
|
||||||
log('warn', configuredLogLevel, message);
|
log('warn', configuredLogLevel, message);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
|
|||||||
@@ -165,11 +165,14 @@ if (entry.argv.includes('--ensure-linux-runtime-plugin-assets')) {
|
|||||||
const pluginDir = path.join(dataDir, 'plugin', 'subminer');
|
const pluginDir = path.join(dataDir, 'plugin', 'subminer');
|
||||||
const pluginConfigPath = path.join(dataDir, 'plugin', 'subminer.conf');
|
const pluginConfigPath = path.join(dataDir, 'plugin', 'subminer.conf');
|
||||||
const themePath = path.join(dataDir, 'themes', 'subminer.rasi');
|
const themePath = path.join(dataDir, 'themes', 'subminer.rasi');
|
||||||
|
const thumbnailerPath = path.join(dataDir, 'thumbnailers', 'subminer-ffmpegthumbnailer.thumbnailer');
|
||||||
fs.mkdirSync(pluginDir, { recursive: true });
|
fs.mkdirSync(pluginDir, { recursive: true });
|
||||||
fs.mkdirSync(path.dirname(themePath), { recursive: true });
|
fs.mkdirSync(path.dirname(themePath), { recursive: true });
|
||||||
|
fs.mkdirSync(path.dirname(thumbnailerPath), { recursive: true });
|
||||||
fs.writeFileSync(path.join(pluginDir, 'main.lua'), '-- smoke plugin\\n');
|
fs.writeFileSync(path.join(pluginDir, 'main.lua'), '-- smoke plugin\\n');
|
||||||
fs.writeFileSync(pluginConfigPath, 'smoke=true\\n');
|
fs.writeFileSync(pluginConfigPath, 'smoke=true\\n');
|
||||||
fs.writeFileSync(themePath, '/* smoke theme */\\n');
|
fs.writeFileSync(themePath, '/* smoke theme */\\n');
|
||||||
|
fs.writeFileSync(thumbnailerPath, '[Thumbnailer Entry]\\n');
|
||||||
if (responsePath) {
|
if (responsePath) {
|
||||||
fs.mkdirSync(path.dirname(responsePath), { recursive: true });
|
fs.mkdirSync(path.dirname(responsePath), { recursive: true });
|
||||||
fs.writeFileSync(responsePath, JSON.stringify({ ok: true, status: 'installed', path: path.join(pluginDir, 'main.lua') }));
|
fs.writeFileSync(responsePath, JSON.stringify({ ok: true, status: 'installed', path: path.join(pluginDir, 'main.lua') }));
|
||||||
@@ -620,11 +623,22 @@ test(
|
|||||||
);
|
);
|
||||||
assert.match(result.stdout, /pause mpv until overlay and tokenization are ready/i);
|
assert.match(result.stdout, /pause mpv until overlay and tokenization are ready/i);
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
assert.match(result.stdout, /managed plugin\/theme assets/i);
|
assert.match(result.stdout, /managed plugin\/theme\/thumbnailer assets/i);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
fs.existsSync(path.join(smokeCase.xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi')),
|
fs.existsSync(path.join(smokeCase.xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi')),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.existsSync(
|
||||||
|
path.join(
|
||||||
|
smokeCase.xdgDataHome,
|
||||||
|
'SubMiner',
|
||||||
|
'thumbnailers',
|
||||||
|
'subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ package() {
|
|||||||
"${pkgdir}/usr/share/SubMiner/plugin/subminer.conf"
|
"${pkgdir}/usr/share/SubMiner/plugin/subminer.conf"
|
||||||
install -Dm644 "${srcdir}/assets/themes/subminer.rasi" \
|
install -Dm644 "${srcdir}/assets/themes/subminer.rasi" \
|
||||||
"${pkgdir}/usr/share/SubMiner/themes/subminer.rasi"
|
"${pkgdir}/usr/share/SubMiner/themes/subminer.rasi"
|
||||||
|
install -Dm644 "${srcdir}/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer" \
|
||||||
|
"${pkgdir}/usr/share/SubMiner/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer"
|
||||||
|
|
||||||
install -dm755 "${pkgdir}/usr/share/SubMiner/plugin/subminer"
|
install -dm755 "${pkgdir}/usr/share/SubMiner/plugin/subminer"
|
||||||
cp -a "${srcdir}/plugin/subminer/." "${pkgdir}/usr/share/SubMiner/plugin/subminer/"
|
cp -a "${srcdir}/plugin/subminer/." "${pkgdir}/usr/share/SubMiner/plugin/subminer/"
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.
|
|
||||||
|
|
||||||
<!-- prerelease-base-version: 0.19.4 -->
|
|
||||||
|
|
||||||
## Highlights
|
|
||||||
### Added
|
|
||||||
- **Library Merge and Move**
|
|
||||||
- Duplicate library cards for the same show can now be combined: select cards in the library grid, choose "Merge Selected," and pick which entry to keep. Sessions, mined cards, and watch time move over, and future episodes stay matched to the merged card.
|
|
||||||
- Episodes can be reassigned to a different library entry with a "→" button on the episode row, fixing cases where a file lands under the wrong title. Manual assignments survive later filename parsing, Jellyfin refreshes, and season repair.
|
|
||||||
- Exact AniList matches with compatible seasons now merge automatically, while fuzzy matches show up as a dismissible "Possible duplicate" prompt instead of merging without confirmation.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- **Anki Audio Generation on Network Drives**
|
|
||||||
- Fixed sentence-audio generation timing out on slow network-mounted video files with many subtitle and font-attachment streams.
|
|
||||||
- Extraction now uses bounded probing and a two-minute budget, and failures show a clear error instead of a cryptic one.
|
|
||||||
- **Duplicate Subtitle Line Stats**
|
|
||||||
- Fixed karaoke openings and animated signs (which record one subtitle event per animation frame) inflating word and kanji counts and skewing "Top Repeated Words." Ordinary repeated dialogue and rewatches are unaffected.
|
|
||||||
- Already-inflated stats can be cleaned up with the new "Duplicates" button in the Vocabulary tab, or `subminer stats cleanup --duplicate-lines` (supports `--dry-run` and `--lookback-days`). Only the affected subtitle lines and vocabulary counts are touched; watch time and lines-seen totals are untouched.
|
|
||||||
- **Overlay Modals on macOS and Windows**
|
|
||||||
- Fixed overlay modals and the stats window opening on the wrong macOS Space, or forcing a Space switch, when mpv is fullscreen. They now open above fullscreen mpv on its current Space.
|
|
||||||
- Modals are now prewarmed on macOS and Windows so shortcuts open them promptly, and Windows keeps the hidden modal responsive between sessions.
|
|
||||||
- **Wayland File Drag-and-Drop**
|
|
||||||
- Fixed dragging subtitle and video files from file managers like Thunar onto the overlay on native Wayland; dropped files are now resolved and sent to mpv.
|
|
||||||
- **Windows Mouse Lag**
|
|
||||||
- Fixed system-wide mouse lag while SubMiner is running on Windows, caused by a global mouse hook and blocking window lookups during click-through tracking.
|
|
||||||
- **Mining Clip Accuracy**
|
|
||||||
- Fixed mined audio and animated image clips sometimes capturing the wrong subtitle line when audio extraction was slow. The clip range is now locked in at the moment of lookup, so audio and image clips always match.
|
|
||||||
- **Linux Notifications**
|
|
||||||
- Character dictionary progress notifications on Linux now update in place instead of flickering off and back on with every status change.
|
|
||||||
- **Stats Delete Performance**
|
|
||||||
- Fixed stats deletes freezing the dashboard; deletes now reliably run off the main thread, with automatic retry if the delete worker crashes.
|
|
||||||
- Deletes, library merges/moves, and AniList reassignments are now much faster because totals are updated incrementally instead of rebuilt from scratch, and no longer erase lifetime totals older than the recent session history.
|
|
||||||
- Session deletes on large libraries dropped from minutes to milliseconds.
|
|
||||||
|
|
||||||
### Docs
|
|
||||||
- **Feature Demos Page**
|
|
||||||
- Hidden the unfinished feature demos page from the documentation sidebar; it's still reachable by direct URL.
|
|
||||||
|
|
||||||
## What's Changed
|
|
||||||
|
|
||||||
- feat(stats): add library entry merge and episode move by @ksyasuda in #190
|
|
||||||
- fix(stats): stop counting duplicate typeset subtitle lines by @ksyasuda in #191
|
|
||||||
- fix(media): tolerate slow MKV audio extraction by @ksyasuda in #195
|
|
||||||
- fix(stats): subtract lifetime totals incrementally on delete by @ksyasuda in #196
|
|
||||||
- fix(anki): snapshot mining media clip timing by @ksyasuda in #197
|
|
||||||
- fix(notifications): replace Linux progress updates in place by @ksyasuda in #198
|
|
||||||
- fix(overlay): support native Wayland file drag-and-drop by @ksyasuda in #199
|
|
||||||
- fix(overlay): keep macOS modal windows on fullscreen Spaces by @ksyasuda in #200
|
|
||||||
- fix(overlay): prevent Windows mouse lag during click-through tracking by @ksyasuda in #201
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
See the README and docs/installation guide for full setup steps.
|
|
||||||
|
|
||||||
## Assets
|
|
||||||
|
|
||||||
- Linux: `SubMiner.AppImage`
|
|
||||||
- macOS: `SubMiner-*.dmg` and `SubMiner-*.zip`
|
|
||||||
- Windows: `SubMiner-*.exe` and `SubMiner-*-win.zip`
|
|
||||||
- Optional extras: `subminer-assets.tar.gz` and the `subminer` launcher
|
|
||||||
|
|
||||||
Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`.
|
|
||||||
@@ -82,6 +82,7 @@ test('update-aur-package updates PKGBUILD and .SRCINFO without makepkg', () => {
|
|||||||
pkgbuild,
|
pkgbuild,
|
||||||
/^\s*install -Dm755 "\$\{srcdir\}\/subminer-\$\{pkgver\}" "\$\{pkgdir\}\/usr\/bin\/subminer"$/m,
|
/^\s*install -Dm755 "\$\{srcdir\}\/subminer-\$\{pkgver\}" "\$\{pkgdir\}\/usr\/bin\/subminer"$/m,
|
||||||
);
|
);
|
||||||
|
assert.match(pkgbuild, /assets\/thumbnailers\/subminer-ffmpegthumbnailer\.thumbnailer/);
|
||||||
assert.match(srcinfo, /^\tpkgver = 0\.6\.3$/m);
|
assert.match(srcinfo, /^\tpkgver = 0\.6\.3$/m);
|
||||||
assert.match(srcinfo, /^\tprovides = subminer=0\.6\.3$/m);
|
assert.match(srcinfo, /^\tprovides = subminer=0\.6\.3$/m);
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ import {
|
|||||||
resolveManagedLinuxRuntimePluginPaths,
|
resolveManagedLinuxRuntimePluginPaths,
|
||||||
} from './linux-runtime-plugin-assets';
|
} from './linux-runtime-plugin-assets';
|
||||||
|
|
||||||
|
const THUMBNAILER_RELATIVE_PATH = path.join(
|
||||||
|
'thumbnailers',
|
||||||
|
'subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
);
|
||||||
|
|
||||||
|
function writeThumbnailer(rootDir: string, content = '[Thumbnailer Entry]\n'): string {
|
||||||
|
const thumbnailerPath = path.join(rootDir, THUMBNAILER_RELATIVE_PATH);
|
||||||
|
fs.mkdirSync(path.dirname(thumbnailerPath), { recursive: true });
|
||||||
|
fs.writeFileSync(thumbnailerPath, content);
|
||||||
|
return thumbnailerPath;
|
||||||
|
}
|
||||||
|
|
||||||
async function withTempDir<T>(fn: (dir: string) => Promise<T> | T): Promise<T> {
|
async function withTempDir<T>(fn: (dir: string) => Promise<T> | T): Promise<T> {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-linux-plugin-assets-test-'));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-linux-plugin-assets-test-'));
|
||||||
try {
|
try {
|
||||||
@@ -48,6 +60,7 @@ test('resolveManagedLinuxRuntimePluginPaths resolves XDG data target paths', ()
|
|||||||
pluginEntrypointPath: '/tmp/xdg-data/SubMiner/plugin/subminer/main.lua',
|
pluginEntrypointPath: '/tmp/xdg-data/SubMiner/plugin/subminer/main.lua',
|
||||||
pluginConfigPath: '/tmp/xdg-data/SubMiner/plugin/subminer.conf',
|
pluginConfigPath: '/tmp/xdg-data/SubMiner/plugin/subminer.conf',
|
||||||
themePath: '/tmp/xdg-data/SubMiner/themes/subminer.rasi',
|
themePath: '/tmp/xdg-data/SubMiner/themes/subminer.rasi',
|
||||||
|
thumbnailerPath: '/tmp/xdg-data/SubMiner/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,6 +92,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin dir, config, and ro
|
|||||||
await withTempDir(async (tempDir) => {
|
await withTempDir(async (tempDir) => {
|
||||||
const sourceRoot = path.join(tempDir, 'source', 'plugin');
|
const sourceRoot = path.join(tempDir, 'source', 'plugin');
|
||||||
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
||||||
|
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
|
||||||
const targetRoot = path.join(tempDir, 'xdg-data', 'SubMiner', 'plugin');
|
const targetRoot = path.join(tempDir, 'xdg-data', 'SubMiner', 'plugin');
|
||||||
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
|
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
|
||||||
fs.mkdirSync(path.dirname(themeSourcePath), { recursive: true });
|
fs.mkdirSync(path.dirname(themeSourcePath), { recursive: true });
|
||||||
@@ -94,6 +108,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin dir, config, and ro
|
|||||||
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
||||||
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
||||||
themeSourcePath,
|
themeSourcePath,
|
||||||
|
thumbnailerSourcePath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,6 +132,19 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin dir, config, and ro
|
|||||||
),
|
),
|
||||||
'/* theme */\n',
|
'/* theme */\n',
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(
|
||||||
|
tempDir,
|
||||||
|
'xdg-data',
|
||||||
|
'SubMiner',
|
||||||
|
'thumbnailers',
|
||||||
|
'subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
'[Thumbnailer Entry]\n',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,6 +152,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme when plugin assets a
|
|||||||
await withTempDir(async (tempDir) => {
|
await withTempDir(async (tempDir) => {
|
||||||
const sourceRoot = path.join(tempDir, 'source', 'plugin');
|
const sourceRoot = path.join(tempDir, 'source', 'plugin');
|
||||||
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
||||||
|
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
|
||||||
const xdgDataHome = path.join(tempDir, 'xdg-data');
|
const xdgDataHome = path.join(tempDir, 'xdg-data');
|
||||||
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
|
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
|
||||||
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
|
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
|
||||||
@@ -143,6 +172,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme when plugin assets a
|
|||||||
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
||||||
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
||||||
themeSourcePath,
|
themeSourcePath,
|
||||||
|
thumbnailerSourcePath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,6 +199,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme when plugin assets a
|
|||||||
test('ensureLinuxRuntimePluginAssets installs managed theme without resolving plugin sources when plugin assets already exist', async () => {
|
test('ensureLinuxRuntimePluginAssets installs managed theme without resolving plugin sources when plugin assets already exist', async () => {
|
||||||
await withTempDir(async (tempDir) => {
|
await withTempDir(async (tempDir) => {
|
||||||
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
||||||
|
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
|
||||||
const xdgDataHome = path.join(tempDir, 'xdg-data');
|
const xdgDataHome = path.join(tempDir, 'xdg-data');
|
||||||
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
|
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
|
||||||
fs.mkdirSync(path.dirname(themeSourcePath), { recursive: true });
|
fs.mkdirSync(path.dirname(themeSourcePath), { recursive: true });
|
||||||
@@ -183,6 +214,7 @@ test('ensureLinuxRuntimePluginAssets installs managed theme without resolving pl
|
|||||||
xdgDataHome,
|
xdgDataHome,
|
||||||
resolveBundledAssets: () => ({
|
resolveBundledAssets: () => ({
|
||||||
themeSourcePath,
|
themeSourcePath,
|
||||||
|
thumbnailerSourcePath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,6 +282,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin assets without reso
|
|||||||
path.join(xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi'),
|
path.join(xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi'),
|
||||||
'/* existing theme */\n',
|
'/* existing theme */\n',
|
||||||
);
|
);
|
||||||
|
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
|
||||||
|
|
||||||
const result = await ensureLinuxRuntimePluginAssets({
|
const result = await ensureLinuxRuntimePluginAssets({
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
@@ -258,6 +291,7 @@ test('ensureLinuxRuntimePluginAssets installs managed plugin assets without reso
|
|||||||
resolveBundledAssets: () => ({
|
resolveBundledAssets: () => ({
|
||||||
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
||||||
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
||||||
|
thumbnailerSourcePath,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -332,6 +366,7 @@ test('ensureLinuxRuntimePluginAssets returns already-present when managed assets
|
|||||||
path.join(xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi'),
|
path.join(xdgDataHome, 'SubMiner', 'themes', 'subminer.rasi'),
|
||||||
'/* theme */\n',
|
'/* theme */\n',
|
||||||
);
|
);
|
||||||
|
writeThumbnailer(path.join(xdgDataHome, 'SubMiner'));
|
||||||
|
|
||||||
const result = await ensureLinuxRuntimePluginAssets({
|
const result = await ensureLinuxRuntimePluginAssets({
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
@@ -369,6 +404,7 @@ test('ensureLinuxRuntimePluginAssets leaves no final target tree on failed insta
|
|||||||
await withTempDir(async (tempDir) => {
|
await withTempDir(async (tempDir) => {
|
||||||
const sourceRoot = path.join(tempDir, 'source', 'plugin');
|
const sourceRoot = path.join(tempDir, 'source', 'plugin');
|
||||||
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
const themeSourcePath = path.join(tempDir, 'source', 'assets', 'themes', 'subminer.rasi');
|
||||||
|
const thumbnailerSourcePath = writeThumbnailer(path.join(tempDir, 'source', 'assets'));
|
||||||
const xdgDataHome = path.join(tempDir, 'xdg-data');
|
const xdgDataHome = path.join(tempDir, 'xdg-data');
|
||||||
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
|
const targetRoot = path.join(xdgDataHome, 'SubMiner', 'plugin');
|
||||||
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
|
fs.mkdirSync(path.join(sourceRoot, 'subminer'), { recursive: true });
|
||||||
@@ -385,6 +421,7 @@ test('ensureLinuxRuntimePluginAssets leaves no final target tree on failed insta
|
|||||||
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
pluginDirSource: path.join(sourceRoot, 'subminer'),
|
||||||
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
pluginConfigSource: path.join(sourceRoot, 'subminer.conf'),
|
||||||
themeSourcePath,
|
themeSourcePath,
|
||||||
|
thumbnailerSourcePath,
|
||||||
}),
|
}),
|
||||||
copyFile: async () => {
|
copyFile: async () => {
|
||||||
throw new Error('copy failed');
|
throw new Error('copy failed');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface ManagedLinuxRuntimePluginPaths {
|
|||||||
pluginEntrypointPath: string;
|
pluginEntrypointPath: string;
|
||||||
pluginConfigPath: string;
|
pluginConfigPath: string;
|
||||||
themePath: string;
|
themePath: string;
|
||||||
|
thumbnailerPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnsureLinuxRuntimePluginAssetsResult {
|
export interface EnsureLinuxRuntimePluginAssetsResult {
|
||||||
@@ -23,6 +24,7 @@ interface RuntimePluginAssetSources {
|
|||||||
pluginDirSource?: string;
|
pluginDirSource?: string;
|
||||||
pluginConfigSource?: string;
|
pluginConfigSource?: string;
|
||||||
themeSourcePath?: string;
|
themeSourcePath?: string;
|
||||||
|
thumbnailerSourcePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuntimePluginDirentLike {
|
interface RuntimePluginDirentLike {
|
||||||
@@ -72,6 +74,11 @@ export function resolveManagedLinuxRuntimePluginPaths(options: {
|
|||||||
pluginEntrypointPath: pathModule.join(pluginDir, 'main.lua'),
|
pluginEntrypointPath: pathModule.join(pluginDir, 'main.lua'),
|
||||||
pluginConfigPath: pathModule.join(rootDir, 'subminer.conf'),
|
pluginConfigPath: pathModule.join(rootDir, 'subminer.conf'),
|
||||||
themePath: pathModule.join(dataDir, 'themes', 'subminer.rasi'),
|
themePath: pathModule.join(dataDir, 'themes', 'subminer.rasi'),
|
||||||
|
thumbnailerPath: pathModule.join(
|
||||||
|
dataDir,
|
||||||
|
'thumbnailers',
|
||||||
|
'subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +102,12 @@ async function copyDirectoryRecursive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBundledThemePath(options: {
|
function resolveBundledAssetPath(options: {
|
||||||
dirname: string;
|
dirname: string;
|
||||||
appPath: string;
|
appPath: string;
|
||||||
resourcesPath: string;
|
resourcesPath: string;
|
||||||
existsSync: (candidate: string) => boolean;
|
existsSync: (candidate: string) => boolean;
|
||||||
|
relativePath: string;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
const roots = [
|
const roots = [
|
||||||
path.join(options.resourcesPath, 'assets'),
|
path.join(options.resourcesPath, 'assets'),
|
||||||
@@ -111,7 +119,7 @@ function resolveBundledThemePath(options: {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const root of roots) {
|
for (const root of roots) {
|
||||||
const candidate = path.join(root, 'themes', 'subminer.rasi');
|
const candidate = path.join(root, options.relativePath);
|
||||||
if (options.existsSync(candidate)) return candidate;
|
if (options.existsSync(candidate)) return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,16 +137,25 @@ function resolveBundledAssetsDefault(
|
|||||||
existsSync,
|
existsSync,
|
||||||
});
|
});
|
||||||
|
|
||||||
const themeSourcePath = resolveBundledThemePath({
|
const themeSourcePath = resolveBundledAssetPath({
|
||||||
dirname: __dirname,
|
dirname: __dirname,
|
||||||
appPath: process.execPath,
|
appPath: process.execPath,
|
||||||
resourcesPath,
|
resourcesPath,
|
||||||
existsSync,
|
existsSync,
|
||||||
|
relativePath: path.join('themes', 'subminer.rasi'),
|
||||||
|
});
|
||||||
|
const thumbnailerSourcePath = resolveBundledAssetPath({
|
||||||
|
dirname: __dirname,
|
||||||
|
appPath: process.execPath,
|
||||||
|
resourcesPath,
|
||||||
|
existsSync,
|
||||||
|
relativePath: path.join('thumbnailers', 'subminer-ffmpegthumbnailer.thumbnailer'),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(pluginAssets ?? {}),
|
...(pluginAssets ?? {}),
|
||||||
...(themeSourcePath ? { themeSourcePath } : {}),
|
...(themeSourcePath ? { themeSourcePath } : {}),
|
||||||
|
...(thumbnailerSourcePath ? { thumbnailerSourcePath } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +195,8 @@ export async function ensureLinuxRuntimePluginAssets(
|
|||||||
const pluginAssetsExist =
|
const pluginAssetsExist =
|
||||||
existsSync(managedPaths.pluginEntrypointPath) && existsSync(managedPaths.pluginConfigPath);
|
existsSync(managedPaths.pluginEntrypointPath) && existsSync(managedPaths.pluginConfigPath);
|
||||||
const themeExists = existsSync(managedPaths.themePath);
|
const themeExists = existsSync(managedPaths.themePath);
|
||||||
if (pluginAssetsExist && themeExists) {
|
const thumbnailerExists = existsSync(managedPaths.thumbnailerPath);
|
||||||
|
if (pluginAssetsExist && themeExists && thumbnailerExists) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 'already-present',
|
status: 'already-present',
|
||||||
@@ -193,6 +211,7 @@ export async function ensureLinuxRuntimePluginAssets(
|
|||||||
|
|
||||||
const shouldInstallPluginAssets = !pluginAssetsExist;
|
const shouldInstallPluginAssets = !pluginAssetsExist;
|
||||||
const shouldInstallTheme = !themeExists;
|
const shouldInstallTheme = !themeExists;
|
||||||
|
const shouldInstallThumbnailer = !thumbnailerExists;
|
||||||
if (
|
if (
|
||||||
shouldInstallPluginAssets &&
|
shouldInstallPluginAssets &&
|
||||||
(!bundledAssets.pluginDirSource || !bundledAssets.pluginConfigSource)
|
(!bundledAssets.pluginDirSource || !bundledAssets.pluginConfigSource)
|
||||||
@@ -210,6 +229,13 @@ export async function ensureLinuxRuntimePluginAssets(
|
|||||||
error: 'Bundled Linux runtime theme asset was not found.',
|
error: 'Bundled Linux runtime theme asset was not found.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (shouldInstallThumbnailer && !bundledAssets.thumbnailerSourcePath) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 'failed',
|
||||||
|
error: 'Bundled Linux rofi thumbnailer asset was not found.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const stagingSuffix = `${process.pid}-${Date.now()}`;
|
const stagingSuffix = `${process.pid}-${Date.now()}`;
|
||||||
const stagedPluginDir = pathModule.join(managedPaths.rootDir, `.subminer-stage-${stagingSuffix}`);
|
const stagedPluginDir = pathModule.join(managedPaths.rootDir, `.subminer-stage-${stagingSuffix}`);
|
||||||
@@ -221,9 +247,14 @@ export async function ensureLinuxRuntimePluginAssets(
|
|||||||
pathModule.dirname(managedPaths.themePath),
|
pathModule.dirname(managedPaths.themePath),
|
||||||
`.subminer.rasi-stage-${stagingSuffix}`,
|
`.subminer.rasi-stage-${stagingSuffix}`,
|
||||||
);
|
);
|
||||||
|
const stagedThumbnailerPath = pathModule.join(
|
||||||
|
pathModule.dirname(managedPaths.thumbnailerPath),
|
||||||
|
`.subminer-ffmpegthumbnailer.thumbnailer-stage-${stagingSuffix}`,
|
||||||
|
);
|
||||||
let pluginDirInstalled = false;
|
let pluginDirInstalled = false;
|
||||||
let pluginConfigInstalled = false;
|
let pluginConfigInstalled = false;
|
||||||
let themeInstalled = false;
|
let themeInstalled = false;
|
||||||
|
let thumbnailerInstalled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (shouldInstallPluginAssets) {
|
if (shouldInstallPluginAssets) {
|
||||||
@@ -249,6 +280,14 @@ export async function ensureLinuxRuntimePluginAssets(
|
|||||||
await mkdir(pathModule.dirname(managedPaths.themePath), { recursive: true });
|
await mkdir(pathModule.dirname(managedPaths.themePath), { recursive: true });
|
||||||
await copyFile(themeSourcePath, stagedThemePath);
|
await copyFile(themeSourcePath, stagedThemePath);
|
||||||
}
|
}
|
||||||
|
if (shouldInstallThumbnailer) {
|
||||||
|
const thumbnailerSourcePath = bundledAssets.thumbnailerSourcePath;
|
||||||
|
if (!thumbnailerSourcePath) {
|
||||||
|
throw new Error('Bundled Linux rofi thumbnailer asset was not found.');
|
||||||
|
}
|
||||||
|
await mkdir(pathModule.dirname(managedPaths.thumbnailerPath), { recursive: true });
|
||||||
|
await copyFile(thumbnailerSourcePath, stagedThumbnailerPath);
|
||||||
|
}
|
||||||
if (shouldInstallPluginAssets) {
|
if (shouldInstallPluginAssets) {
|
||||||
await rm(managedPaths.pluginDir, { recursive: true, force: true });
|
await rm(managedPaths.pluginDir, { recursive: true, force: true });
|
||||||
await rm(managedPaths.pluginConfigPath, { force: true });
|
await rm(managedPaths.pluginConfigPath, { force: true });
|
||||||
@@ -262,6 +301,11 @@ export async function ensureLinuxRuntimePluginAssets(
|
|||||||
await rename(stagedThemePath, managedPaths.themePath);
|
await rename(stagedThemePath, managedPaths.themePath);
|
||||||
themeInstalled = true;
|
themeInstalled = true;
|
||||||
}
|
}
|
||||||
|
if (shouldInstallThumbnailer) {
|
||||||
|
await rm(managedPaths.thumbnailerPath, { force: true });
|
||||||
|
await rename(stagedThumbnailerPath, managedPaths.thumbnailerPath);
|
||||||
|
thumbnailerInstalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -278,9 +322,13 @@ export async function ensureLinuxRuntimePluginAssets(
|
|||||||
if (themeInstalled) {
|
if (themeInstalled) {
|
||||||
await rm(managedPaths.themePath, { force: true }).catch(() => {});
|
await rm(managedPaths.themePath, { force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
if (thumbnailerInstalled) {
|
||||||
|
await rm(managedPaths.thumbnailerPath, { force: true }).catch(() => {});
|
||||||
|
}
|
||||||
await rm(stagedPluginDir, { recursive: true, force: true }).catch(() => {});
|
await rm(stagedPluginDir, { recursive: true, force: true }).catch(() => {});
|
||||||
await rm(stagedPluginConfigPath, { force: true }).catch(() => {});
|
await rm(stagedPluginConfigPath, { force: true }).catch(() => {});
|
||||||
await rm(stagedThemePath, { force: true }).catch(() => {});
|
await rm(stagedThemePath, { force: true }).catch(() => {});
|
||||||
|
await rm(stagedThumbnailerPath, { force: true }).catch(() => {});
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from './support-assets';
|
} from './support-assets';
|
||||||
|
|
||||||
type SupportAssetsResultWithComponent = SupportAssetsUpdateResult & {
|
type SupportAssetsResultWithComponent = SupportAssetsUpdateResult & {
|
||||||
component?: 'theme' | 'plugin';
|
component?: 'theme' | 'thumbnailer' | 'plugin';
|
||||||
};
|
};
|
||||||
|
|
||||||
function sha256(data: Buffer): string {
|
function sha256(data: Buffer): string {
|
||||||
@@ -22,18 +22,30 @@ function sha256(data: Buffer): string {
|
|||||||
|
|
||||||
function makeSupportAssetsArchive(options?: {
|
function makeSupportAssetsArchive(options?: {
|
||||||
themeContent?: string;
|
themeContent?: string;
|
||||||
|
thumbnailerContent?: string;
|
||||||
|
includeThumbnailer?: boolean;
|
||||||
pluginVersion?: string | null;
|
pluginVersion?: string | null;
|
||||||
pluginMainContent?: string;
|
pluginMainContent?: string;
|
||||||
extraPluginFiles?: Array<{ relativePath: string; content: string }>;
|
extraPluginFiles?: Array<{ relativePath: string; content: string }>;
|
||||||
}): { archive: Buffer; tempDir: string } {
|
}): { archive: Buffer; tempDir: string } {
|
||||||
const themeContent = options?.themeContent ?? 'new theme\n';
|
const themeContent = options?.themeContent ?? 'new theme\n';
|
||||||
|
const thumbnailerContent = options?.thumbnailerContent ?? '[Thumbnailer Entry]\n';
|
||||||
const pluginVersion = options && 'pluginVersion' in options ? options.pluginVersion : '0.12.0';
|
const pluginVersion = options && 'pluginVersion' in options ? options.pluginVersion : '0.12.0';
|
||||||
const pluginMainContent = options?.pluginMainContent ?? 'new plugin\n';
|
const pluginMainContent = options?.pluginMainContent ?? 'new plugin\n';
|
||||||
const extraPluginFiles = options?.extraPluginFiles ?? [];
|
const extraPluginFiles = options?.extraPluginFiles ?? [];
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-support-assets-test-'));
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-support-assets-test-'));
|
||||||
fs.mkdirSync(path.join(tempDir, 'assets/themes'), { recursive: true });
|
fs.mkdirSync(path.join(tempDir, 'assets/themes'), { recursive: true });
|
||||||
|
if (options?.includeThumbnailer !== false) {
|
||||||
|
fs.mkdirSync(path.join(tempDir, 'assets/thumbnailers'), { recursive: true });
|
||||||
|
}
|
||||||
fs.mkdirSync(path.join(tempDir, 'plugin/subminer'), { recursive: true });
|
fs.mkdirSync(path.join(tempDir, 'plugin/subminer'), { recursive: true });
|
||||||
fs.writeFileSync(path.join(tempDir, 'assets/themes/subminer.rasi'), themeContent);
|
fs.writeFileSync(path.join(tempDir, 'assets/themes/subminer.rasi'), themeContent);
|
||||||
|
if (options?.includeThumbnailer !== false) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
|
||||||
|
thumbnailerContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
fs.writeFileSync(path.join(tempDir, 'plugin/subminer/main.lua'), pluginMainContent);
|
fs.writeFileSync(path.join(tempDir, 'plugin/subminer/main.lua'), pluginMainContent);
|
||||||
if (pluginVersion !== null) {
|
if (pluginVersion !== null) {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -93,7 +105,7 @@ test('detectSupportAssetDataDirs only returns Linux support-asset locations', ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildProtectedSupportAssetsCommand installs both theme and plugin assets', () => {
|
test('buildProtectedSupportAssetsCommand installs theme, thumbnailer, and plugin assets', () => {
|
||||||
const command = buildProtectedSupportAssetsCommand(
|
const command = buildProtectedSupportAssetsCommand(
|
||||||
"https://example.test/subminer assets.tar.gz?sig='abc'",
|
"https://example.test/subminer assets.tar.gz?sig='abc'",
|
||||||
'ABCDEF1234',
|
'ABCDEF1234',
|
||||||
@@ -110,11 +122,28 @@ test('buildProtectedSupportAssetsCommand installs both theme and plugin assets',
|
|||||||
command,
|
command,
|
||||||
/printf '%s %s\\n' 'abcdef1234' "\$tmp\/subminer-assets\.tar\.gz" \| sha256sum -c -/,
|
/printf '%s %s\\n' 'abcdef1234' "\$tmp\/subminer-assets\.tar\.gz" \| sha256sum -c -/,
|
||||||
);
|
);
|
||||||
|
const requiredAssetChecks = [
|
||||||
|
'test -f "$tmp/assets/themes/subminer.rasi"',
|
||||||
|
'test -f "$tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer"',
|
||||||
|
'test -f "$tmp/plugin/subminer/main.lua"',
|
||||||
|
'test -f "$tmp/plugin/subminer/version.lua"',
|
||||||
|
];
|
||||||
|
const firstSudoIndex = command.indexOf('sudo ');
|
||||||
|
assert.notEqual(firstSudoIndex, -1);
|
||||||
|
for (const check of requiredAssetChecks) {
|
||||||
|
const checkIndex = command.indexOf(check);
|
||||||
|
assert.notEqual(checkIndex, -1);
|
||||||
|
assert.ok(checkIndex < firstSudoIndex);
|
||||||
|
}
|
||||||
assert.match(command, /sudo mkdir -p '\/usr\/local\/share\/SubMiner'\\''s data'\/themes/);
|
assert.match(command, /sudo mkdir -p '\/usr\/local\/share\/SubMiner'\\''s data'\/themes/);
|
||||||
assert.match(
|
assert.match(
|
||||||
command,
|
command,
|
||||||
/sudo cp "\$tmp\/assets\/themes\/subminer\.rasi" '\/usr\/local\/share\/SubMiner'\\''s data'\/themes\/subminer\.rasi/,
|
/sudo cp "\$tmp\/assets\/themes\/subminer\.rasi" '\/usr\/local\/share\/SubMiner'\\''s data'\/themes\/subminer\.rasi/,
|
||||||
);
|
);
|
||||||
|
assert.match(
|
||||||
|
command,
|
||||||
|
/sudo cp "\$tmp\/assets\/thumbnailers\/subminer-ffmpegthumbnailer\.thumbnailer" .*thumbnailers\/subminer-ffmpegthumbnailer\.thumbnailer/,
|
||||||
|
);
|
||||||
assert.match(command, /sudo mkdir -p '\/usr\/local\/share\/SubMiner'\\''s data'\/plugin/);
|
assert.match(command, /sudo mkdir -p '\/usr\/local\/share\/SubMiner'\\''s data'\/plugin/);
|
||||||
assert.match(command, /sudo rm -rf .*plugin\/subminer\.next/);
|
assert.match(command, /sudo rm -rf .*plugin\/subminer\.next/);
|
||||||
assert.match(command, /sudo cp -R "\$tmp\/plugin\/subminer" .*plugin\/subminer\.next/);
|
assert.match(command, /sudo cp -R "\$tmp\/plugin\/subminer" .*plugin\/subminer\.next/);
|
||||||
@@ -209,6 +238,12 @@ test('updateSupportAssetsFromRelease installs missing plugin into a root with a
|
|||||||
path: dataDir,
|
path: dataDir,
|
||||||
message: 'Updated theme.',
|
message: 'Updated theme.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
status: 'updated',
|
||||||
|
component: 'thumbnailer',
|
||||||
|
path: dataDir,
|
||||||
|
message: 'Installed rofi thumbnailer.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 'updated',
|
status: 'updated',
|
||||||
component: 'plugin',
|
component: 'plugin',
|
||||||
@@ -220,6 +255,13 @@ test('updateSupportAssetsFromRelease installs missing plugin into a root with a
|
|||||||
fs.readFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'utf8'),
|
fs.readFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'utf8'),
|
||||||
'new theme\n',
|
'new theme\n',
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
'[Thumbnailer Entry]\n',
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
fs.readFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'utf8'),
|
fs.readFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'utf8'),
|
||||||
'new plugin\n',
|
'new plugin\n',
|
||||||
@@ -345,8 +387,13 @@ test('updateSupportAssetsFromRelease skips identical theme and up-to-date plugin
|
|||||||
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
|
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
|
||||||
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
|
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
|
||||||
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
|
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(dataDir, 'thumbnailers'), { recursive: true });
|
||||||
fs.mkdirSync(path.join(dataDir, 'plugin/subminer'), { recursive: true });
|
fs.mkdirSync(path.join(dataDir, 'plugin/subminer'), { recursive: true });
|
||||||
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'same theme\n');
|
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'same theme\n');
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
|
||||||
|
'[Thumbnailer Entry]\n',
|
||||||
|
);
|
||||||
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'same plugin\n');
|
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'same plugin\n');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(dataDir, 'plugin/subminer/version.lua'),
|
path.join(dataDir, 'plugin/subminer/version.lua'),
|
||||||
@@ -371,6 +418,12 @@ test('updateSupportAssetsFromRelease skips identical theme and up-to-date plugin
|
|||||||
path: dataDir,
|
path: dataDir,
|
||||||
message: 'Theme already up to date.',
|
message: 'Theme already up to date.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
status: 'skipped',
|
||||||
|
component: 'thumbnailer',
|
||||||
|
path: dataDir,
|
||||||
|
message: 'Rofi thumbnailer already up to date.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
component: 'plugin',
|
component: 'plugin',
|
||||||
@@ -396,8 +449,13 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
|
|||||||
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
|
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
|
||||||
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
|
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
|
||||||
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
|
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(dataDir, 'thumbnailers'), { recursive: true });
|
||||||
fs.mkdirSync(path.join(dataDir, 'plugin/subminer'), { recursive: true });
|
fs.mkdirSync(path.join(dataDir, 'plugin/subminer'), { recursive: true });
|
||||||
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'old theme\n');
|
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'old theme\n');
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
|
||||||
|
'[Old Thumbnailer]\n',
|
||||||
|
);
|
||||||
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'old plugin\n');
|
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'old plugin\n');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(dataDir, 'plugin/subminer/version.lua'),
|
path.join(dataDir, 'plugin/subminer/version.lua'),
|
||||||
@@ -406,6 +464,7 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
|
|||||||
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/stale.lua'), 'stale\n');
|
fs.writeFileSync(path.join(dataDir, 'plugin/subminer/stale.lua'), 'stale\n');
|
||||||
const { archive, tempDir } = makeSupportAssetsArchive({
|
const { archive, tempDir } = makeSupportAssetsArchive({
|
||||||
themeContent: 'new theme\n',
|
themeContent: 'new theme\n',
|
||||||
|
thumbnailerContent: '[Thumbnailer Entry]\n',
|
||||||
pluginVersion: '0.12.0',
|
pluginVersion: '0.12.0',
|
||||||
pluginMainContent: 'new plugin main\n',
|
pluginMainContent: 'new plugin main\n',
|
||||||
extraPluginFiles: [{ relativePath: 'fresh.lua', content: 'fresh\n' }],
|
extraPluginFiles: [{ relativePath: 'fresh.lua', content: 'fresh\n' }],
|
||||||
@@ -424,6 +483,12 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
|
|||||||
path: dataDir,
|
path: dataDir,
|
||||||
message: 'Updated theme.',
|
message: 'Updated theme.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
status: 'updated',
|
||||||
|
component: 'thumbnailer',
|
||||||
|
path: dataDir,
|
||||||
|
message: 'Updated rofi thumbnailer.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 'updated',
|
status: 'updated',
|
||||||
component: 'plugin',
|
component: 'plugin',
|
||||||
@@ -435,6 +500,13 @@ test('updateSupportAssetsFromRelease updates changed theme and outdated plugin w
|
|||||||
fs.readFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'utf8'),
|
fs.readFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'utf8'),
|
||||||
'new theme\n',
|
'new theme\n',
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(dataDir, 'thumbnailers/subminer-ffmpegthumbnailer.thumbnailer'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
'[Thumbnailer Entry]\n',
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
fs.readFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'utf8'),
|
fs.readFileSync(path.join(dataDir, 'plugin/subminer/main.lua'), 'utf8'),
|
||||||
'new plugin main\n',
|
'new plugin main\n',
|
||||||
@@ -479,6 +551,12 @@ test('updateSupportAssetsFromRelease returns protected commands for managed root
|
|||||||
path: dataDir,
|
path: dataDir,
|
||||||
command: true,
|
command: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
status: 'protected',
|
||||||
|
component: 'thumbnailer',
|
||||||
|
path: dataDir,
|
||||||
|
command: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 'protected',
|
status: 'protected',
|
||||||
component: 'plugin',
|
component: 'plugin',
|
||||||
@@ -488,6 +566,10 @@ test('updateSupportAssetsFromRelease returns protected commands for managed root
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
assert.match(results[0]?.command ?? '', /themes\/subminer\.rasi/);
|
assert.match(results[0]?.command ?? '', /themes\/subminer\.rasi/);
|
||||||
|
assert.match(
|
||||||
|
results[0]?.command ?? '',
|
||||||
|
/thumbnailers\/subminer-ffmpegthumbnailer\.thumbnailer/,
|
||||||
|
);
|
||||||
assert.match(results[0]?.command ?? '', /plugin\/subminer/);
|
assert.match(results[0]?.command ?? '', /plugin\/subminer/);
|
||||||
} finally {
|
} finally {
|
||||||
fs.chmodSync(dataDir, originalMode);
|
fs.chmodSync(dataDir, originalMode);
|
||||||
@@ -522,3 +604,25 @@ test('updateSupportAssetsFromRelease returns missing-asset when release plugin v
|
|||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('updateSupportAssetsFromRelease rejects archives without the rofi thumbnailer', async () => {
|
||||||
|
const xdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-xdg-data-'));
|
||||||
|
const dataDir = path.posix.join(xdgDataHome, 'SubMiner');
|
||||||
|
fs.mkdirSync(path.join(dataDir, 'themes'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dataDir, 'themes/subminer.rasi'), 'managed theme\n');
|
||||||
|
const { archive, tempDir } = makeSupportAssetsArchive({ includeThumbnailer: false });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await runLinuxSupportAssetUpdate({ archive, xdgDataHome });
|
||||||
|
|
||||||
|
assert.deepEqual(results, [
|
||||||
|
{
|
||||||
|
status: 'missing-asset',
|
||||||
|
message: 'Support asset archive is missing the rofi thumbnailer.',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(xdgDataHome, { recursive: true, force: true });
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ import { compareSemverLike, findReleaseAsset } from './release-assets';
|
|||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const THEME_RELATIVE_PATH = path.join('themes', 'subminer.rasi');
|
const THEME_RELATIVE_PATH = path.join('themes', 'subminer.rasi');
|
||||||
|
const THUMBNAILER_RELATIVE_PATH = path.join(
|
||||||
|
'thumbnailers',
|
||||||
|
'subminer-ffmpegthumbnailer.thumbnailer',
|
||||||
|
);
|
||||||
const PLUGIN_ENTRYPOINT_RELATIVE_PATH = path.join('plugin', 'subminer', 'main.lua');
|
const PLUGIN_ENTRYPOINT_RELATIVE_PATH = path.join('plugin', 'subminer', 'main.lua');
|
||||||
const PLUGIN_VERSION_RELATIVE_PATH = path.join('plugin', 'subminer', 'version.lua');
|
const PLUGIN_VERSION_RELATIVE_PATH = path.join('plugin', 'subminer', 'version.lua');
|
||||||
const PLUGIN_DIR_RELATIVE_PATH = path.join('plugin', 'subminer');
|
const PLUGIN_DIR_RELATIVE_PATH = path.join('plugin', 'subminer');
|
||||||
|
|
||||||
export interface SupportAssetsUpdateResult {
|
export interface SupportAssetsUpdateResult {
|
||||||
status: 'updated' | 'skipped' | 'protected' | 'hash-mismatch' | 'missing-asset';
|
status: 'updated' | 'skipped' | 'protected' | 'hash-mismatch' | 'missing-asset';
|
||||||
component?: 'theme' | 'plugin';
|
component?: 'theme' | 'thumbnailer' | 'plugin';
|
||||||
path?: string;
|
path?: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -69,11 +73,12 @@ async function readInstalledPluginVersion(pluginDir: string): Promise<string | n
|
|||||||
async function detectManagedSupportAssetDataDirs(dataDirs: string[]): Promise<string[]> {
|
async function detectManagedSupportAssetDataDirs(dataDirs: string[]): Promise<string[]> {
|
||||||
const managedDataDirs: string[] = [];
|
const managedDataDirs: string[] = [];
|
||||||
for (const dataDir of dataDirs) {
|
for (const dataDir of dataDirs) {
|
||||||
const [hasTheme, hasPlugin] = await Promise.all([
|
const [hasTheme, hasThumbnailer, hasPlugin] = await Promise.all([
|
||||||
pathExists(path.join(dataDir, THEME_RELATIVE_PATH)),
|
pathExists(path.join(dataDir, THEME_RELATIVE_PATH)),
|
||||||
|
pathExists(path.join(dataDir, THUMBNAILER_RELATIVE_PATH)),
|
||||||
pathExists(path.join(dataDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH)),
|
pathExists(path.join(dataDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH)),
|
||||||
]);
|
]);
|
||||||
if (hasTheme || hasPlugin) {
|
if (hasTheme || hasThumbnailer || hasPlugin) {
|
||||||
managedDataDirs.push(dataDir);
|
managedDataDirs.push(dataDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,8 +168,14 @@ export function buildProtectedSupportAssetsCommand(
|
|||||||
`curl -fSL ${shellQuote(assetUrl)} -o "$tmp/subminer-assets.tar.gz"`,
|
`curl -fSL ${shellQuote(assetUrl)} -o "$tmp/subminer-assets.tar.gz"`,
|
||||||
`printf '%s %s\\n' ${quotedExpectedSha256} "$tmp/subminer-assets.tar.gz" | sha256sum -c -`,
|
`printf '%s %s\\n' ${quotedExpectedSha256} "$tmp/subminer-assets.tar.gz" | sha256sum -c -`,
|
||||||
'tar -xzf "$tmp/subminer-assets.tar.gz" -C "$tmp"',
|
'tar -xzf "$tmp/subminer-assets.tar.gz" -C "$tmp"',
|
||||||
|
'test -f "$tmp/assets/themes/subminer.rasi"',
|
||||||
|
'test -f "$tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer"',
|
||||||
|
'test -f "$tmp/plugin/subminer/main.lua"',
|
||||||
|
'test -f "$tmp/plugin/subminer/version.lua"',
|
||||||
`sudo mkdir -p ${quotedDir}/themes`,
|
`sudo mkdir -p ${quotedDir}/themes`,
|
||||||
`sudo cp "$tmp/assets/themes/subminer.rasi" ${quotedDir}/themes/subminer.rasi`,
|
`sudo cp "$tmp/assets/themes/subminer.rasi" ${quotedDir}/themes/subminer.rasi`,
|
||||||
|
`sudo mkdir -p ${quotedDir}/thumbnailers`,
|
||||||
|
`sudo cp "$tmp/assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer" ${quotedDir}/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer`,
|
||||||
`sudo mkdir -p ${quotedDir}/plugin`,
|
`sudo mkdir -p ${quotedDir}/plugin`,
|
||||||
`sudo rm -rf ${quotedStagedPluginDir} ${quotedBackupPluginDir}`,
|
`sudo rm -rf ${quotedStagedPluginDir} ${quotedBackupPluginDir}`,
|
||||||
`sudo cp -R "$tmp/plugin/subminer" ${quotedStagedPluginDir}`,
|
`sudo cp -R "$tmp/plugin/subminer" ${quotedStagedPluginDir}`,
|
||||||
@@ -216,6 +227,12 @@ export async function updateSupportAssetsFromRelease(options: {
|
|||||||
dataDir,
|
dataDir,
|
||||||
'Support asset path is not a directory.',
|
'Support asset path is not a directory.',
|
||||||
),
|
),
|
||||||
|
makeSupportAssetResult(
|
||||||
|
'skipped',
|
||||||
|
'thumbnailer',
|
||||||
|
dataDir,
|
||||||
|
'Support asset path is not a directory.',
|
||||||
|
),
|
||||||
makeSupportAssetResult(
|
makeSupportAssetResult(
|
||||||
'skipped',
|
'skipped',
|
||||||
'plugin',
|
'plugin',
|
||||||
@@ -244,6 +261,13 @@ export async function updateSupportAssetsFromRelease(options: {
|
|||||||
'Theme install requires a manual command.',
|
'Theme install requires a manual command.',
|
||||||
command,
|
command,
|
||||||
),
|
),
|
||||||
|
makeSupportAssetResult(
|
||||||
|
'protected',
|
||||||
|
'thumbnailer',
|
||||||
|
dataDir,
|
||||||
|
'Rofi thumbnailer install requires a manual command.',
|
||||||
|
command,
|
||||||
|
),
|
||||||
makeSupportAssetResult(
|
makeSupportAssetResult(
|
||||||
'protected',
|
'protected',
|
||||||
'plugin',
|
'plugin',
|
||||||
@@ -284,6 +308,17 @@ export async function updateSupportAssetsFromRelease(options: {
|
|||||||
}
|
}
|
||||||
const themeBytes = await fs.promises.readFile(themeSourcePath);
|
const themeBytes = await fs.promises.readFile(themeSourcePath);
|
||||||
|
|
||||||
|
const thumbnailerSourcePath = path.join(tempDir, 'assets', THUMBNAILER_RELATIVE_PATH);
|
||||||
|
if (!(await pathExists(thumbnailerSourcePath))) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
status: 'missing-asset',
|
||||||
|
message: 'Support asset archive is missing the rofi thumbnailer.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const thumbnailerBytes = await fs.promises.readFile(thumbnailerSourcePath);
|
||||||
|
|
||||||
const sourcePluginDir = path.join(tempDir, PLUGIN_DIR_RELATIVE_PATH);
|
const sourcePluginDir = path.join(tempDir, PLUGIN_DIR_RELATIVE_PATH);
|
||||||
const sourcePluginEntrypoint = path.join(tempDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH);
|
const sourcePluginEntrypoint = path.join(tempDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH);
|
||||||
if (!(await pathExists(sourcePluginEntrypoint))) {
|
if (!(await pathExists(sourcePluginEntrypoint))) {
|
||||||
@@ -328,6 +363,33 @@ export async function updateSupportAssetsFromRelease(options: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetThumbnailerPath = path.join(dataDir, THUMBNAILER_RELATIVE_PATH);
|
||||||
|
const existingThumbnailerBytes = await readFileIfExists(targetThumbnailerPath);
|
||||||
|
if (
|
||||||
|
existingThumbnailerBytes &&
|
||||||
|
Buffer.compare(existingThumbnailerBytes, thumbnailerBytes) === 0
|
||||||
|
) {
|
||||||
|
results.push(
|
||||||
|
makeSupportAssetResult(
|
||||||
|
'skipped',
|
||||||
|
'thumbnailer',
|
||||||
|
dataDir,
|
||||||
|
'Rofi thumbnailer already up to date.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await fs.promises.mkdir(path.dirname(targetThumbnailerPath), { recursive: true });
|
||||||
|
await fs.promises.writeFile(targetThumbnailerPath, thumbnailerBytes);
|
||||||
|
results.push(
|
||||||
|
makeSupportAssetResult(
|
||||||
|
'updated',
|
||||||
|
'thumbnailer',
|
||||||
|
dataDir,
|
||||||
|
existingThumbnailerBytes ? 'Updated rofi thumbnailer.' : 'Installed rofi thumbnailer.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const targetPluginDir = path.join(dataDir, PLUGIN_DIR_RELATIVE_PATH);
|
const targetPluginDir = path.join(dataDir, PLUGIN_DIR_RELATIVE_PATH);
|
||||||
const targetPluginEntrypoint = path.join(dataDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH);
|
const targetPluginEntrypoint = path.join(dataDir, PLUGIN_ENTRYPOINT_RELATIVE_PATH);
|
||||||
const installedPluginVersion = await readInstalledPluginVersion(targetPluginDir);
|
const installedPluginVersion = await readInstalledPluginVersion(targetPluginDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user