Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
371 lines
23 KiB
JavaScript
371 lines
23 KiB
JavaScript
// Round thirty-six: one file, picked. The storage plugin had a folder
|
|
// picker and an image picker and no way at all to name a FILE, and the
|
|
// phone's import sheet has been four doors with a note under it apologising
|
|
// for the fifth since round 24. So `pick_file`: the document sheet with the
|
|
// caller's own filter, answered with a path Rust can open — the document's
|
|
// OWN path where this phone has one (nothing copied, so a 300 MB pick costs
|
|
// nothing), a bounded copy into cacheDir/picked under its own name where it
|
|
// does not, and the display name handed over separately because that name
|
|
// is what decides a playlist's format and its title. Then the fifth door,
|
|
// on round 24's screen, wait card and review; `playlist_import_probe` took
|
|
// a path and a name the way `playlist_import_folder` took a path.
|
|
//
|
|
// The brief also named a second consumer — `community_pick_file`, "sending
|
|
// a file in a chat". It does not exist: `community_pick_file` is the YOU
|
|
// section's three native dialogs (an avatar source, and the .ssrecovery
|
|
// artifact), the protocol wrapper has `say_room` and `message_user` and
|
|
// nothing that sends a file, and both frontends' thread composers are text
|
|
// only. Nothing is wired for it, and nothing is invented.
|
|
const fs = require('fs');
|
|
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
|
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
|
|
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
|
|
let p, s;
|
|
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
|
const save = () => fs.writeFileSync(root + p, s);
|
|
const once = (a) => {
|
|
const i = s.indexOf(a);
|
|
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
|
|
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
|
|
return i;
|
|
};
|
|
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
|
|
const NL = String.fromCharCode(10);
|
|
/* Two tolerant anchors, both in ANDROID.md, both for the same reason:
|
|
playlist EXPORT is named in the same breath as this round's picker twice
|
|
over — the "Not on the phone yet" sentence and the "Not done yet" bullet
|
|
— and export is a sibling round's line to close. Whichever of the two
|
|
rounds ran first, what is left behind has to be true, so each takes
|
|
either shape and throws only when neither is there. Every anchor outside
|
|
ANDROID.md is hard, and so is every other anchor inside it. */
|
|
const either = (a, b, c, d) => {
|
|
if (s.includes(a)) rep(a, b);
|
|
else if (s.includes(c)) rep(c, d);
|
|
else throw new Error('missing (neither shape) in ' + p + ': ' + a.slice(0, 90));
|
|
};
|
|
|
|
/* Several inserts below re-emit their own anchors, and one file is written
|
|
whole, so a second run would double-write where `once` cannot see it
|
|
coming and only throw further down, leaving half a tree. Refuse one here,
|
|
before anything is touched: the plugin's COMMANDS array is what this
|
|
round creates and nothing else in the repo makes it. */
|
|
if (fs.readFileSync(root + 'crates/tauri-plugin-storage/build.rs', 'utf8').includes('pick_file')) {
|
|
throw new Error('crates/tauri-plugin-storage already has pick_file — round 36 has already run against ' + root);
|
|
}
|
|
|
|
// ── the plugin: Kotlin ────────────────────────────────────────────────
|
|
load('crates/tauri-plugin-storage/android/src/main/java/com/soulstream/storage/StoragePlugin.kt');
|
|
/* Round thirty-five lands before this one and needs the same two imports for
|
|
its own document sheet, so take them only if they are not already there —
|
|
a file cannot import a name twice for the sake of a second caller. */
|
|
if (!s.includes(`import app.tauri.annotation.InvokeArg`)) {
|
|
rep(`import app.tauri.annotation.Command`,
|
|
`import app.tauri.annotation.Command
|
|
import app.tauri.annotation.InvokeArg`);
|
|
}
|
|
if (!s.includes(`import android.provider.OpenableColumns`)) {
|
|
rep(`import android.provider.DocumentsContract`,
|
|
`import android.provider.DocumentsContract
|
|
import android.provider.OpenableColumns`);
|
|
}
|
|
/* the args class sits at top level, where transfer-notice's NoticeArgs does
|
|
— above the plugin's own KDoc, so that comment stays on the class it
|
|
describes */
|
|
rep(`/**
|
|
* The phone's storage for the library: a folder picked through Android's`,
|
|
ins('ins-r36-kt-top.txt') + NL + NL + `/**
|
|
* The phone's storage for the library: a folder picked through Android's`);
|
|
/* the class tail. folderPicked ends with the same two lines, but only
|
|
imagePicked's are followed by the class's own closing brace. */
|
|
rep(` Activity.RESULT_CANCELED -> invoke.reject("cancelled")
|
|
else -> invoke.reject("the picker failed")
|
|
}
|
|
}
|
|
}`,
|
|
` Activity.RESULT_CANCELED -> invoke.reject("cancelled")
|
|
else -> invoke.reject("the picker failed")
|
|
}
|
|
}
|
|
|
|
` + ins('ins-r36-kt-body.txt') + NL + `}`);
|
|
save();
|
|
|
|
// ── the plugin: Rust ──────────────────────────────────────────────────
|
|
load('crates/tauri-plugin-storage/src/lib.rs');
|
|
rep(`//! Three commands: \`pick_folder\` → \`{ uri, path }\` (path null when the tree
|
|
//! is not on a volume with a path), \`all_files_access\` → \`{ granted }\`, and
|
|
//! \`request_all_files_access\`, which opens the system screen. On the desktop
|
|
//! the plugin is registered so the permission set resolves, and every
|
|
//! command answers "not here".`, ins('ins-r36-plugin-doc.txt'));
|
|
rep(`#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PickedImage {`, ins('ins-r36-plugin-types.txt') + NL + NL + `#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PickedImage {`);
|
|
rep(`/// A photo from the phone: the document picker limited to images, the`,
|
|
ins('ins-r36-plugin-cmd.txt') + NL + NL + `/// A photo from the phone: the document picker limited to images, the`);
|
|
rep(` .invoke_handler(tauri::generate_handler![pick_folder, all_files_access, request_all_files_access, pick_image])`,
|
|
` .invoke_handler(tauri::generate_handler![pick_folder, all_files_access, request_all_files_access, pick_image, pick_file])`);
|
|
save();
|
|
|
|
load('crates/tauri-plugin-storage/build.rs');
|
|
rep(`// The three commands, as the Rust functions are named; the ACL slugs them
|
|
// (\`allow-pick-folder\`, …) into permissions/autogenerated.
|
|
const COMMANDS: &[&str] = &["pick_folder", "all_files_access", "request_all_files_access", "pick_image"];`,
|
|
`// The commands, as the Rust functions are named; the ACL slugs them
|
|
// (\`allow-pick-folder\`, …) into permissions/autogenerated.
|
|
const COMMANDS: &[&str] = &["pick_folder", "all_files_access", "request_all_files_access", "pick_image", "pick_file"];`);
|
|
save();
|
|
|
|
// ── the permission set. The three files under autogenerated/ are the
|
|
// plugin build script's own output, emitted here verbatim rather than
|
|
// hand-written, which is how they drift. ─────────────────────────────
|
|
load('crates/tauri-plugin-storage/permissions/default.toml');
|
|
rep(`The phone's storage: pick a library folder (a Storage Access Framework tree,
|
|
resolved to a path when the volume allows), and Android's all-files access —
|
|
whether it is granted, and the system screen that grants it.
|
|
"""
|
|
permissions = ["allow-pick-folder", "allow-all-files-access", "allow-request-all-files-access", "allow-pick-image"]`,
|
|
`The phone's storage: pick a library folder (a Storage Access Framework tree,
|
|
resolved to a path when the volume allows), pick a photo or a file (the
|
|
document picker, answered with a path — the document's own where this phone
|
|
has one, else a bounded copy in the app's cache), and Android's all-files
|
|
access — whether it is granted, and the system screen that grants it.
|
|
"""
|
|
permissions = ["allow-pick-folder", "allow-all-files-access", "allow-request-all-files-access", "allow-pick-image", "allow-pick-file"]`);
|
|
save();
|
|
fs.writeFileSync(root + 'crates/tauri-plugin-storage/permissions/autogenerated/commands/pick_file.toml',
|
|
ins('ins-r36-perm-command.txt') + NL);
|
|
fs.writeFileSync(root + 'crates/tauri-plugin-storage/permissions/autogenerated/reference.md',
|
|
ins('ins-r36-perm-reference.txt') + NL);
|
|
/* the schema ends on its last brace, with no newline — the generator's own
|
|
shape, and a byte the next real build would otherwise rewrite */
|
|
fs.writeFileSync(root + 'crates/tauri-plugin-storage/permissions/schemas/schema.json',
|
|
ins('ins-r36-perm-schema.txt'));
|
|
|
|
// ── the Rust the door lands on: the probe grows a path AND a name, the
|
|
// way round 24's folder import grew a path ───────────────────────────
|
|
load('src-tauri/src/playlists.rs');
|
|
rep(`/// Open a playlist file and stage it for review. Parsing and resolution only —
|
|
/// nothing is written until \`playlist_import_commit\`.
|
|
#[tauri::command]
|
|
pub async fn playlist_import_probe(
|
|
app: tauri::AppHandle,
|
|
lib: State<'_, LibraryState>,
|
|
) -> Result<Option<ImportDraft>, String> {
|
|
use tauri_plugin_dialog::DialogExt;
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
app.dialog()
|
|
.file()
|
|
.add_filter(
|
|
"Playlist",
|
|
&["m3u", "m3u8", "pls", "xspf", "csv", "json", "txt"],
|
|
)
|
|
.pick_file(move |path| {
|
|
let _ = tx.send(path.map(|p| p.to_string()));
|
|
});
|
|
let Some(source) = rx.await.unwrap_or(None) else {
|
|
return Ok(None);
|
|
};
|
|
let source_path = PathBuf::from(&source);
|
|
let name = source_path
|
|
.file_name()
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
.unwrap_or_else(|| source.clone());`, ins('ins-r36-probe-head.txt'));
|
|
save();
|
|
|
|
// ── the wrappers ──────────────────────────────────────────────────────
|
|
load('src/lib/api.js');
|
|
rep(`export const pickImage = () => invoke("plugin:storage|pick_image");`,
|
|
`export const pickImage = () => invoke("plugin:storage|pick_image");
|
|
// A FILE from the phone (the storage plugin): { path, name, size, copied }.
|
|
// \`mimeTypes\` is the document sheet's filter, comma-separated; \`maxCopyBytes\`
|
|
// is how big a document may be copied into the app's cache when it has no
|
|
// path of its own on this phone — 0 refuses the copy, which is what an
|
|
// audio-sized pick wants. The name comes back beside the path because a
|
|
// copy is named after the document and only the document's name is it.
|
|
export const pickFile = (mimeTypes, maxCopyBytes = 0) =>
|
|
invoke("plugin:storage|pick_file", { mimeTypes, maxCopyBytes });`);
|
|
rep(`// Import: probe opens a file dialog and stages a reviewed draft; text stages
|
|
// a pasted tracklist; commit writes the accepted draft in one transaction.
|
|
export const playlistImportProbe = () => invoke("playlist_import_probe");`,
|
|
`// Import: probe stages a reviewed draft from a playlist file; text stages
|
|
// a pasted tracklist; commit writes the accepted draft in one transaction.
|
|
// The desktop passes neither argument and opens its own dialog; the phone
|
|
// picks with the storage plugin and hands the path in — with the picked
|
|
// document's own name, which is what chooses the parser and titles the
|
|
// draft (a copied file is not always named what the document was).
|
|
export const playlistImportProbe = (path = null, name = null) =>
|
|
invoke("playlist_import_probe", { path, name });`);
|
|
save();
|
|
|
|
// ── the fifth door ────────────────────────────────────────────────────
|
|
load('src/mobile/importer.svelte.js');
|
|
rep(` rules, on the doors that need no file picker: a link (YouTube · Spotify ·
|
|
Apple Music · TIDAL · Deezer), a pasted tracklist (plain lines, a
|
|
timestamped setlist, or track links — a paste holding one is read as
|
|
links), a YouTube DJ set's posted tracklist, and a folder of audio files`,
|
|
` rules, on every door it has: a playlist file off this phone (M3U · M3U8 ·
|
|
PLS · XSPF · CSV · JSON), a link (YouTube · Spotify · Apple Music ·
|
|
TIDAL · Deezer), a pasted tracklist (plain lines, a timestamped setlist,
|
|
or track links — a paste holding one is read as links), a YouTube DJ
|
|
set's posted tracklist, and a folder of audio files`);
|
|
rep(` door: null, // "link" | "text" | "youtube" | "folder"`,
|
|
` door: null, // "file" | "link" | "text" | "youtube" | "folder"`);
|
|
rep(`/** [door, icon, title, hint] — the desktop's Import menu, minus the
|
|
* playlist-file door (that one still wants a file picker). A YouTube
|
|
* playlist link goes through "link": the backend dispatches it. */
|
|
export const DOORS = [`,
|
|
`/** [door, icon, title, hint] — the desktop's Import menu, all five doors.
|
|
* A YouTube playlist link goes through "link": the backend dispatches it. */
|
|
export const DOORS = [
|
|
["file", "file-text", "Playlist file…", "M3U · M3U8 · PLS · XSPF · CSV · JSON"],`);
|
|
rep(`/** The folder door picks FIRST — the phone's own document-tree picker — so`,
|
|
ins('ins-r36-importer-pick.txt') + NL + `/** The folder door picks FIRST — the phone's own document-tree picker — so`);
|
|
rep(`/** Stage a folder of audio files. The backend walks the folder and its disc`,
|
|
ins('ins-r36-importer-start.txt') + NL + `/** Stage a folder of audio files. The backend walks the folder and its disc`);
|
|
save();
|
|
|
|
load('src/mobile/HalfSheet.svelte');
|
|
rep(` import { DOORS, start as startImport, pickImportFolder, startFolder } from "./importer.svelte.js";`,
|
|
` import { DOORS, start as startImport, pickImportFolder, startFolder, pickImportFile, startFile } from "./importer.svelte.js";`);
|
|
rep(` /* the folder door picks FIRST: the review only opens for a folder that`,
|
|
ins('ins-r36-halfsheet-door.txt') + NL + ` /* the folder door picks FIRST: the review only opens for a folder that`);
|
|
rep(` {@render row(icon, t, m, () => (door === "folder" ? folderDoor() : importPrompt(door)))}`,
|
|
` {@render row(icon, t, m, () => (door === "folder" ? folderDoor() : door === "file" ? fileDoor() : importPrompt(door)))}`);
|
|
/* the note that apologised for the missing door says the one true thing
|
|
about the door instead: a file copied onto this phone has lost its own
|
|
folder, and the rows in it match by filename after that */
|
|
rep(` <div class="sec-note" style="padding-bottom: 14px">A playlist file — M3U, XSPF, PLS or CSV — imports on the desktop; the phone has no file picker for one yet.</div>`,
|
|
` <div class="sec-note" style="padding-bottom: 14px">A playlist file's rows are matched against the library on this phone — by path where the file names one, and by filename otherwise, so a file copied here from another machine still finds what you have.</div>`);
|
|
save();
|
|
|
|
load('src/mobile/screens/Import.svelte');
|
|
rep(` : importer.door === "folder" ? "Reading the folder…"`,
|
|
` : importer.door === "file" ? "Reading the playlist file…"
|
|
: importer.door === "folder" ? "Reading the folder…"`);
|
|
save();
|
|
|
|
load('src/mobile/Mobile.svelte');
|
|
rep(` import { importer, bindImporter, startFolder } from "./importer.svelte.js";`,
|
|
` import { importer, bindImporter, startFolder, startFile } from "./importer.svelte.js";`);
|
|
rep(`imp: { startFolder },`, `imp: { startFolder, startFile },`);
|
|
save();
|
|
|
|
// ── the mock: the picker, and the probe behind it ─────────────────────
|
|
load('src/lib/mockLibrary.js');
|
|
rep(` if (cmd === "plugin:storage|pick_image") return Promise.resolve({ path: "C:\\\\mock\\\\photo.jpg" });`,
|
|
` if (cmd === "plugin:storage|pick_image") return Promise.resolve({ path: "C:\\\\mock\\\\photo.jpg" });
|
|
/* a document that HAD to be copied, with the two names apart: the cache
|
|
file is safeName()'s — every character outside the plugin's allowed
|
|
set replaced, so the "#" is an underscore there — and the document's
|
|
own is what the draft must be read and titled by. Same on purpose:
|
|
if the name stopped travelling, the path's basename would still say
|
|
m3u8 and only the title would quietly go wrong, which is the bug. */
|
|
if (cmd === "plugin:storage|pick_file") return Promise.resolve({ path: "C:\\\\mock\\\\picked\\\\Warehouse set _3.m3u8", name: "Warehouse set #3.m3u8", size: 2048, copied: true });`);
|
|
rep(` if (cmd === "playlist_import_folder") {`,
|
|
ins('ins-r36-mock-probe.txt') + NL + ` if (cmd === "playlist_import_folder") {`);
|
|
save();
|
|
|
|
// ── the walks. Both blocks sit INSIDE round 24's import run rather than
|
|
// at the array ends, which every sibling round reaches for. ─────────
|
|
load('tests/mobile/smoke.mjs');
|
|
rep(` impBtn: (document.querySelector('.imp-foot .btn-fill') || {}).textContent || '',`,
|
|
` impBtn: (document.querySelector('.imp-foot .btn-fill') || {}).textContent || '',
|
|
impSrc: (document.querySelector('.imp-src') || {}).textContent || '',`);
|
|
/* the share-cancel line is byte-identical in both walks, so the anchor
|
|
takes the line after it too */
|
|
rep(' { do: `__click(\'.imp-foot .btn-ghost\', null)`, name: "share-cancel", wait: 900 },' + NL
|
|
+ ' { do: `document.querySelectorAll(\'.segtabs button\')[0].click()`, wait: 400 },',
|
|
' { do: `__click(\'.imp-foot .btn-ghost\', null)`, name: "share-cancel", wait: 900 },' + NL
|
|
+ ins('ins-r36-mock-steps.txt') + NL
|
|
+ ' { do: `document.querySelectorAll(\'.segtabs button\')[0].click()`, wait: 400 },');
|
|
/* "share-cancel" reads identically in both maps, so this anchor takes the
|
|
line after it as well */
|
|
rep(` "share-cancel": (i) => i.title.startsWith("Your library"),
|
|
"cat-album-row":`,
|
|
` "share-cancel": (i) => i.title.startsWith("Your library"),
|
|
` + ins('ins-r36-mock-expect.txt') + NL + ` "cat-album-row":`);
|
|
rep(' { do: `__click(\'.halfsheet .row\', \'Folder of audio files\')`, name: "import-folder-picker", wait: 2600 },' + NL
|
|
+ ' { adb: ["shell", "input", "keyevent", "4"], name: "import-folder-cancelled", wait: 2000 },',
|
|
' { do: `__click(\'.halfsheet .row\', \'Folder of audio files\')`, name: "import-folder-picker", wait: 2600 },' + NL
|
|
+ ' { adb: ["shell", "input", "keyevent", "4"], name: "import-folder-cancelled", wait: 2000 },' + NL
|
|
+ ins('ins-r36-phone-steps.txt'));
|
|
rep(` "import-folder-cancelled": (i) => i.title.startsWith("Your library") && !i.half.includes("up") && !i.toast,`,
|
|
` "import-folder-cancelled": (i) => i.title.startsWith("Your library") && !i.half.includes("up") && !i.toast,
|
|
` + ins('ins-r36-phone-expect.txt'));
|
|
save();
|
|
|
|
// ── the doc ───────────────────────────────────────────────────────────
|
|
load('ANDROID.md');
|
|
rep(`is outside the app's private data and access is missing). Nothing is read
|
|
outside the folder the user picked.`,
|
|
`is outside the app's private data and access is missing). Nothing is read
|
|
outside the folder the user picked.
|
|
|
|
` + ins('ins-r36-doc-storage.txt'));
|
|
rep(` shelf's Import opens four doors — a link (YouTube · Spotify · Apple`,
|
|
` shelf's Import opens five doors — a playlist file on this phone (M3U ·
|
|
M3U8 · PLS · XSPF · CSV · JSON), a link (YouTube · Spotify · Apple`);
|
|
rep(` \`playlist_import_folder\`, which grew a \`path\` argument for it: on the
|
|
phone that command had no dialog to open and always answered nothing.`,
|
|
` \`playlist_import_folder\`, which grew a \`path\` argument for it: on the
|
|
phone that command had no dialog to open and always answered nothing. The
|
|
file door does the same through the plugin's \`pick_file\`, and
|
|
\`playlist_import_probe\` grew \`path\` AND \`name\` for it — the picked
|
|
document's own display name is what \`detect_format\` reads the extension
|
|
off and what titles the draft, and a document this phone had no path for
|
|
arrives as a cache copy under a sanitized filename. A copied file's rows
|
|
still find the library: a relative locator that misses falls back to the
|
|
basename index, which is the one thing that reads differently from the
|
|
desktop, and the sheet says so.`);
|
|
/* export is named beside the file door here, so the sibling round that
|
|
ships export rewrites this sentence before this one reaches it — and it
|
|
re-wraps the lines when it does. Both shapes are answered, and BOTH
|
|
answers end on the line
|
|
"yt-dlp, a desktop tool), drag reorder, and Share on SoulStream."
|
|
byte for byte, because that line is the drag-reorder round's own anchor
|
|
and it is the third round to want this one sentence. Only the clause this
|
|
round closes comes out; the rest is left where it was found. */
|
|
either(`Not on the phone yet: the playlist-file door and export — both want a file
|
|
picker the storage plugin has still to grow — the listener door (it needs
|
|
yt-dlp, a desktop tool), drag reorder, and Share on SoulStream.`,
|
|
`Not on the phone yet: export — it wants a file picker the storage plugin
|
|
has still to grow, or a writer for one — the listener door (it needs
|
|
yt-dlp, a desktop tool), drag reorder, and Share on SoulStream.`,
|
|
`Not on the phone yet: the playlist-file door — it wants a file picker the
|
|
storage plugin has still to grow — the listener door (it needs yt-dlp, a
|
|
desktop tool), drag reorder, and Share on SoulStream.`,
|
|
`Not on the phone yet: the listener door (it needs
|
|
yt-dlp, a desktop tool), drag reorder, and Share on SoulStream.`);
|
|
rep(`links read a page each, a folder of audio files off the phone, a share
|
|
from another app) → the playlist repair (an unknown row relinked from the
|
|
library)`,
|
|
/* the line "library) → the artwork viewer → …" is left where it is, whole:
|
|
the drag-reorder round anchors on it. So the wrap above it absorbs the
|
|
new clause instead. */
|
|
`links read a page each, a folder of audio files off the phone, a playlist
|
|
file through the phone's own document picker — with that picker cancelled
|
|
and a file it can hand no path for refused — and a share from another
|
|
app) → the playlist repair (an unknown row relinked from the
|
|
library)`);
|
|
/* and the bullet itself. Its other half is export again: while export is
|
|
still waiting, only this round's sentence comes out and the bullet stands.
|
|
Once the sibling round has shipped export the bullet is nothing BUT this
|
|
sentence, and a "waiting on a picker" with nothing waiting — and a plugin
|
|
that "cannot read one" when it can — is worse than no bullet, so the
|
|
whole thing goes. */
|
|
const BULLET_AFTER_EXPORT = `- **Waiting on a picker.** The storage plugin can WRITE a document now
|
|
(\`create_document\`, which the playlist export goes through) but it cannot
|
|
read one. Playlist-FILE import wants a \`pick_file\` in the storage plugin,
|
|
beside the \`pick_folder\` and \`pick_image\` it already has.
|
|
`;
|
|
const LINE_THIS_ROUND = ` Playlist-FILE import wants a \`pick_file\` in the storage plugin,
|
|
beside the \`pick_folder\` and \`pick_image\` it already has.`;
|
|
if (s.includes(BULLET_AFTER_EXPORT)) rep(BULLET_AFTER_EXPORT, '');
|
|
else if (s.includes(LINE_THIS_ROUND)) rep(LINE_THIS_ROUND, '');
|
|
else console.log(' (round 36: the pick_file line had already gone from "Not done yet")');
|
|
save();
|
|
|
|
console.log('round 36 wired');
|