bin/seed
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env bash
set -e
database=""
from_date=""
db_path="$HOME/.local/share/slskd/data"
dry_run=false
clear_db=false
backup=false
while test $# -gt 0; do
case "$1" in
-h|--help)
echo ''
echo 'usage: seed --database <name> --from <date> [options]'
echo ''
echo 'options:'
echo '-h, --help show help'
echo '--database NAME database to seed; supported: transfers'
echo '--from DATE seed records starting from this date (YYYY-MM-DD)'
echo '--db-path PATH path to database directory'
echo " default: \$HOME/.local/share/slskd/data"
echo '--backup back up the database before making any changes'
echo '--clear delete all existing records before seeding'
echo '--dry-run show what would be seeded without writing'
echo ''
exit 0
;;
--database)
shift
database=$1
shift
;;
--from)
shift
from_date=$1
shift
;;
--db-path)
shift
db_path=$1
shift
;;
--backup)
backup=true
shift
;;
--clear)
clear_db=true
shift
;;
--dry-run)
dry_run=true
shift
;;
*)
break
;;
esac
done
if [ -z "$database" ]; then
echo 'error: --database is required'
exit 1
fi
if [ -z "$from_date" ]; then
echo 'error: --from is required'
exit 1
fi
case "$database" in
transfers)
echo $'\n\t🌱 seeding transfers database\n'
if [ "$backup" == true ] && [ "$dry_run" == false ]; then
ts=$(date -u '+%m%d%y_%H%M%S')
backup_file="$db_path/backups/transfers.pre-seed-backup.$ts.db"
cp "$db_path/transfers.db" "$backup_file"
echo $'\t💾 backed up to '$(basename "$backup_file")$'\n'
fi
FROM_DATE="$from_date" DB_PATH="$db_path" DRY_RUN="$dry_run" CLEAR_DB="$clear_db" node --no-warnings << 'JS'
'use strict';
const { DatabaseSync } = require('node:sqlite');
const { randomUUID } = require('node:crypto');
const path = require('node:path');
const fromDate = new Date(process.env.FROM_DATE);
const dbPath = process.env.DB_PATH;
const dryRun = process.env.DRY_RUN === 'true';
const clearDb = process.env.CLEAR_DB === 'true';
const dbFile = path.join(dbPath, 'transfers.db');
// ── random helpers ────────────────────────────────────────────────────────────
const rand = (lo, hi) => Math.random() * (hi - lo) + lo;
const randInt = (lo, hi) => Math.floor(rand(lo, hi + 1));
const choice = (arr) => arr[randInt(0, arr.length - 1)];
const sample = (arr, n) => [...arr].sort(() => Math.random() - 0.5).slice(0, n);
const weightedChoice = (choices, weights) => {
let r = Math.random() * weights.reduce((a, b) => a + b, 0);
for (let i = 0; i < choices.length; i++) { r -= weights[i]; if (r <= 0) return choices[i]; }
return choices[choices.length - 1];
};
// ── name generation ───────────────────────────────────────────────────────────
const SYLLABLES = [
'al','an','ar','ax','az','ber','bor','cal','cor','dal','dar','dex',
'dor','dral','ech','el','en','er','eth','ex','fal','fen','fer','fir',
'for','gal','gar','gel','gen','gor','hal','har','hel','hor','il','in',
'ir','ix','kel','ker','kor','kral','lan','lar','lel','len','ler','lir',
'lor','mal','mar','mel','mir','mor','nal','nar','nel','ner','nir','nor',
'ol','on','or','pal','par','pel','per','pir','por','qal','ral','ran',
'rel','ren','rer','rir','ror','sal','sar','sel','ser','sir','sor','tal',
'tar','tel','ter','tir','tor','ul','un','ur','val','var','vel','ver',
'vir','vor','wal','war','wel','wer','xal','xar','xel','yer','yor','zel',
'zen','zer','zir','zor',
];
const ADJECTIVES = [
'amber','ancient','ashen','black','blank','blind','broken','burnt',
'calm','cold','coral','dark','dead','deep','dim','distant','dull',
'dust','empty','errant','fallen','false','far','feral','flat','fold',
'grey','grim','hollow','idle','inner','inverted','iron','jade','last',
'late','lean','light','lone','lost','low','muted','narrow','near',
'neon','numb','obscure','old','pale','phantom','quiet','raw','remote',
'rigid','rusted','salt','shallow','sharp','sheer','silent','slow',
'soft','still','strange','thin','tired','twin','twilight','void',
'wandering','warm','weathered','white','wide','winter','worn',
];
const NOUNS = [
'arc','ash','beacon','bone','breach','cable','canal','carbon','carrier',
'cell','chamber','chord','circuit','coast','code','coil','core','crest',
'current','curve','decay','delta','depth','desert','digit','dome','door',
'drift','echo','edge','epoch','field','filament','floor','flow','fold',
'form','frame','gap','gate','glass','gradient','grain','grid','haze',
'helix','hollow','horizon','hull','index','inlet','iris','junction',
'kelp','layer','ledge','lens','light','line','loop','margin','membrane',
'mesh','mirror','mode','motion','mount','node','null','orbit','origin',
'oxide','peak','phase','pillar','plane','plate','point','pool','portal',
'prism','pulse','radius','rail','range','reef','relay','ridge','ring',
'root','route','ruin','shelf','shell','shore','signal','sink','slope',
'span','sphere','spiral','stack','static','stem','stone','strand','stratum',
'stream','surface','surge','terminal','terrain','tide','tower','trace',
'tract','transit','trench','tunnel','valley','vector','vertex','void',
'voltage','wave','wire','zone',
];
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
const sylWord = (n = 2) => Array.from({ length: n }, () => choice(SYLLABLES)).join('');
const makeUsername = () => {
const sep = choice(['-', '_']);
switch (randInt(0, 3)) {
case 0: return `${choice(ADJECTIVES)}${sep}${choice(NOUNS)}`;
case 1: return `${choice(ADJECTIVES)}${sep}${choice(NOUNS)}${randInt(2, 99)}`;
case 2: return `${sylWord(2)}${randInt(10, 999)}`;
default: return `${sylWord(2)}${choice(['-', '_', ''])}${sylWord(2)}`;
}
};
const makeArtist = () => {
switch (randInt(0, 3)) {
case 0: return `${cap(sylWord(2))} ${cap(sylWord(2))}`;
case 1: return `The ${cap(sylWord(2))}s`;
case 2: return cap(sylWord(randInt(3, 5)));
default: return `${cap(sylWord(2))} & ${cap(sylWord(2))}`;
}
};
const makeAlbumName = () => {
switch (randInt(0, 4)) {
case 0: return `${cap(choice(ADJECTIVES))} ${cap(choice(NOUNS))}`;
case 1: return cap(choice(NOUNS));
case 2: return `${cap(choice(ADJECTIVES))} ${cap(choice(ADJECTIVES))}`;
case 3: return cap(sylWord(3));
default: return `${cap(choice(NOUNS))} ${randInt(1, 9)}`;
}
};
const makeTrackName = (n) => {
const num = String(n).padStart(2, '0');
switch (randInt(0, 3)) {
case 0: return `${num} - ${cap(choice(ADJECTIVES))} ${cap(choice(NOUNS))}.flac`;
case 1: return `${num} - ${cap(sylWord(randInt(2, 4)))}.flac`;
case 2: return `${num} - ${cap(choice(NOUNS))} ${randInt(1, 9)}.flac`;
default: return `${num} - ${cap(choice(ADJECTIVES))}.flac`;
}
};
const makeAlbum = (artist) => {
const nTracks = randInt(5, 16);
const tracks = Array.from({ length: nTracks }, (_, i) => [makeTrackName(i + 1), randInt(8_000_000, 55_000_000)]);
if (Math.random() < 0.85) tracks.push(['folder.jpg', randInt(28_000, 90_000)]);
return { artist, album: makeAlbumName(), tracks };
};
// ── constants ─────────────────────────────────────────────────────────────────
const EXCEPTIONS = [
'A validation error has occurred.',
'Transfer timed out.',
"The remote user's connection was reset.",
'The remote user disconnected.',
"The remote user's queue is full.",
];
const STATE_SUCCEEDED = 48; // Completed | Succeeded (16 | 32)
const STATE_ERRORED = 272; // Completed | Errored (16 | 256)
const HOUR_WEIGHTS = [
3, 4, 4, 3, 2, 1, // 00-05 late night
1, 1, 1, 1, 1, 2, // 06-11 morning
2, 2, 2, 2, 2, 3, // 12-17 afternoon
4, 5, 5, 5, 4, 3, // 18-23 evening
];
// ── helpers ───────────────────────────────────────────────────────────────────
const uid = () => randomUUID().toUpperCase();
const ts = (ms) => {
const d = new Date(ms);
const pad = (n, len = 2) => String(n).padStart(len, '0');
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ` +
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}.` +
`${pad(d.getUTCMilliseconds(), 3)}0000`;
};
// ── seed ──────────────────────────────────────────────────────────────────────
const artistPool = Array.from({ length: randInt(12, 20) }, makeArtist);
const albumPool = Array.from({ length: randInt(30, 60) }, () => makeAlbum(choice(artistPool)));
const userPool = Array.from({ length: randInt(10, 20) }, makeUsername);
const now = Date.now();
const records = [];
const addSession = (direction, album, peer, sessionStart) => {
let cursor = sessionStart + rand(8_000, 60_000);
for (const [trackName, size] of album.tracks) {
const filename = `${album.artist}\\${album.album}\\${trackName}`;
const requestedAt = sessionStart + rand(0, 2_500);
const enqueuedAt = requestedAt + rand(100, 500);
if (Math.random() < 0.05) {
const endedAt = requestedAt + rand(40, 300);
if (endedAt >= now) return;
records.push([
uid(), peer, direction, filename, size,
STATE_ERRORED, 'Completed, Errored',
ts(requestedAt), null, null, ts(endedAt),
0, 0.0, null, choice(EXCEPTIONS),
0, null, 0, null,
]);
} else {
const isCover = trackName.endsWith('.jpg') || trackName.endsWith('.png');
const speed = isCover ? rand(200_000, 600_000) : rand(3_500_000, 9_500_000);
const startedAt = cursor + rand(50, 300);
const endedAt = startedAt + (size / speed) * 1_000;
if (endedAt >= now) return;
records.push([
uid(), peer, direction, filename, size,
STATE_SUCCEEDED, 'Completed, Succeeded',
ts(requestedAt), ts(enqueuedAt), ts(startedAt), ts(endedAt),
size, speed, null, null,
0, null, 1, null,
]);
cursor = endedAt;
}
}
};
let current = new Date(fromDate);
current.setUTCHours(0, 0, 0, 0);
while (current.getTime() < now) {
const nDownloads = weightedChoice([0, 1, 2, 3, 4], [0.10, 0.25, 0.35, 0.20, 0.10]);
const nUploads = weightedChoice([0, 1, 2, 3, 4], [0.15, 0.30, 0.30, 0.20, 0.05]);
for (const [direction, nSessions] of [['Download', nDownloads], ['Upload', nUploads]]) {
for (let i = 0; i < nSessions; i++) {
const hour = weightedChoice([...Array(24).keys()], HOUR_WEIGHTS);
const sessionStart = current.getTime()
+ hour * 3_600_000 + randInt(0, 59) * 60_000 + randInt(0, 59) * 1_000 + randInt(0, 999);
if (sessionStart >= now) continue;
const album = choice(albumPool);
const peer = choice(userPool);
let tracks = album.tracks;
if (direction === 'Upload' && Math.random() < 0.6) {
tracks = sample(tracks, randInt(1, Math.max(1, tracks.length - 1)));
}
addSession(direction, { ...album, tracks }, peer, sessionStart);
}
}
current.setUTCDate(current.getUTCDate() + 1);
}
process.stdout.write(`\n\t📊 ${records.length} records from ${fromDate.toISOString().slice(0, 10)} to ${new Date(now).toISOString().slice(0, 10)}\n`);
if (dryRun) {
process.stdout.write('\n\t⏩ dry run; nothing written\n\n');
process.exit(0);
}
const db = new DatabaseSync(dbFile);
const stmt = db.prepare(`
INSERT INTO Transfers (
Id, Username, Direction, Filename, Size,
State, StateDescription,
RequestedAt, EnqueuedAt, StartedAt, EndedAt,
BytesTransferred, AverageSpeed, PlaceInQueue, Exception,
Removed, BatchId, Attempts, NextAttemptAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
if (clearDb) {
db.exec('DELETE FROM Transfers; DELETE FROM Batches;');
process.stdout.write('\n\t🗑️ cleared existing records\n');
}
db.exec('BEGIN');
for (const r of records) stmt.run(...r);
db.exec('COMMIT');
db.close();
process.stdout.write(`\n\t🎉 seeded ${records.length} records into ${dbFile}\n\n`);
JS
;;
*)
echo "error: unknown database '$database'; supported: transfers"
exit 1
;;
esac