diff --git a/src/lib/db.js b/src/lib/db.js index 9ea591a..9bcb928 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -7,10 +7,7 @@ export function openDb() { dbPromise ??= new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { - const db = request.result; - db.createObjectStore('outliers', { keyPath: 'videoId' }).createIndex('updatedAt', 'updatedAt'); - db.createObjectStore('channels', { keyPath: 'channelId' }); - db.createObjectStore('seen', { keyPath: 'videoId' }); + request.result.createObjectStore('outliers', { keyPath: 'videoId' }).createIndex('updatedAt', 'updatedAt'); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); @@ -28,19 +25,19 @@ function promisify(request) { }); } -async function getMany(storeName, keys) { +export async function getOutliers(videoIds) { const db = await openDb(); - const store = db.transaction(storeName, 'readonly').objectStore(storeName); - const records = await Promise.all(keys.map((key) => promisify(store.get(key)))); - return new Map(records.filter(Boolean).map((record) => [record[store.keyPath], record])); + const store = db.transaction('outliers', 'readonly').objectStore('outliers'); + const records = await Promise.all(videoIds.map((id) => promisify(store.get(id)))); + return new Map(records.filter(Boolean).map((record) => [record.videoId, record])); } -async function putMany(storeName, records) { +export async function putOutliers(records) { if (records.length === 0) return; const db = await openDb(); await new Promise((resolve, reject) => { - const tx = db.transaction(storeName, 'readwrite'); - const store = tx.objectStore(storeName); + const tx = db.transaction('outliers', 'readwrite'); + const store = tx.objectStore('outliers'); for (const record of records) store.put(record); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); @@ -48,13 +45,7 @@ async function putMany(storeName, records) { }); } -export const getSeen = (videoIds) => getMany('seen', videoIds); -export const putSeen = (records) => putMany('seen', records); -export const getChannels = (channelIds) => getMany('channels', channelIds); -export const putChannels = (records) => putMany('channels', records); -export const getOutliers = (videoIds) => getMany('outliers', videoIds); -export const putOutliers = (records) => putMany('outliers', records); - +// Newest `updatedAt` first. export async function listOutliers(limit = Infinity) { const db = await openDb(); return new Promise((resolve, reject) => { diff --git a/src/lib/export.js b/src/lib/export.js index 3d2b6d2..71c1330 100644 --- a/src/lib/export.js +++ b/src/lib/export.js @@ -3,16 +3,12 @@ export const COLUMNS = [ 'title', 'url', 'thumbnailUrl', - 'channelId', 'channelTitle', 'channelUrl', 'subscribers', 'views', - 'publishedAt', - 'durationSec', + 'outlierScore', 'isShort', - 'score', - 'vidiqScore', 'foundOn', 'firstSeenAt', 'updatedAt', diff --git a/src/lib/storage.js b/src/lib/storage.js index 7cbc100..d260e6d 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -1,4 +1,4 @@ -export const DEFAULT_SETTINGS = { apiKey: '', threshold: 5, minViews: 10000 }; +export const DEFAULT_SETTINGS = { threshold: 5, minViews: 10000 }; const toNumber = (value) => (value === null || value === undefined || value === '' ? NaN : Number(value)); @@ -6,7 +6,6 @@ export function normalizeSettings(raw = {}) { const threshold = toNumber(raw.threshold); const minViews = toNumber(raw.minViews); return { - apiKey: typeof raw.apiKey === 'string' ? raw.apiKey.trim() : DEFAULT_SETTINGS.apiKey, threshold: Number.isFinite(threshold) && threshold > 0 ? threshold : DEFAULT_SETTINGS.threshold, minViews: Number.isFinite(minViews) && minViews >= 0 ? Math.floor(minViews) : DEFAULT_SETTINGS.minViews, }; @@ -22,13 +21,3 @@ export async function saveSettings(raw) { await chrome.storage.local.set({ settings }); return settings; } - -// Status is { state: 'ok' | 'no-key' | 'invalid-key' | 'quota', message?, until? }. -export async function getStatus() { - const { status } = await chrome.storage.local.get('status'); - return status ?? { state: 'ok' }; -} - -export async function setStatus(status) { - await chrome.storage.local.set({ status }); -} diff --git a/test/export.test.js b/test/export.test.js index 24e3f25..0c4a879 100644 --- a/test/export.test.js +++ b/test/export.test.js @@ -6,33 +6,45 @@ const record = (over = {}) => ({ videoId: 'dQw4w9WgXcQ', title: 'Hello', url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - thumbnailUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg', - channelId: 'UC1', + thumbnailUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg', channelTitle: 'Chan', channelUrl: 'https://www.youtube.com/@chan', subscribers: 1000, views: 50000, - publishedAt: '2026-10-01T00:00:00Z', - durationSec: 600, + outlierScore: 50, isShort: false, - score: 50, - vidiqScore: null, foundOn: 'home', firstSeenAt: '2026-10-25T10:00:00.000Z', updatedAt: '2026-10-25T10:00:00.000Z', ...over, }); +test('COLUMNS lists the stored fields in order', () => { + assert.deepEqual(COLUMNS, [ + 'videoId', + 'title', + 'url', + 'thumbnailUrl', + 'channelTitle', + 'channelUrl', + 'subscribers', + 'views', + 'outlierScore', + 'isShort', + 'foundOn', + 'firstSeenAt', + 'updatedAt', + ]); +}); + test('toCsv writes a header and one row per record', () => { - const csv = toCsv([record()]); assert.equal( - csv, + toCsv([record()]), COLUMNS.join(',') + '\r\n' + 'dQw4w9WgXcQ,Hello,https://www.youtube.com/watch?v=dQw4w9WgXcQ,' + - 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg,UC1,Chan,https://www.youtube.com/@chan,' + - '1000,50000,2026-10-01T00:00:00Z,600,false,50,,home,' + - '2026-10-25T10:00:00.000Z,2026-10-25T10:00:00.000Z\r\n', + 'https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg,Chan,https://www.youtube.com/@chan,' + + '1000,50000,50,false,home,2026-10-25T10:00:00.000Z,2026-10-25T10:00:00.000Z\r\n', ); }); @@ -42,10 +54,9 @@ test('toCsv quotes commas, quotes and newlines', () => { }); test('toCsv writes null and undefined as empty cells and keeps booleans readable', () => { - const csv = toCsv([record({ subscribers: null, vidiqScore: 7.5, isShort: true, channelUrl: undefined })]); + const csv = toCsv([record({ subscribers: null, isShort: true, channelUrl: undefined })]); const row = csv.split('\r\n')[1]; assert.ok(row.includes(',true,')); - assert.ok(row.includes(',7.5,')); assert.ok(row.includes('Chan,,,50000')); // channelUrl and subscribers empty }); diff --git a/test/storage.test.js b/test/storage.test.js index 9712546..7181e8c 100644 --- a/test/storage.test.js +++ b/test/storage.test.js @@ -3,17 +3,13 @@ import assert from 'node:assert/strict'; import { DEFAULT_SETTINGS, normalizeSettings } from '../src/lib/storage.js'; test('normalizeSettings falls back to defaults for missing input', () => { + assert.deepEqual(DEFAULT_SETTINGS, { threshold: 5, minViews: 10000 }); assert.deepEqual(normalizeSettings(), DEFAULT_SETTINGS); assert.deepEqual(normalizeSettings({}), DEFAULT_SETTINGS); - assert.deepEqual(DEFAULT_SETTINGS, { apiKey: '', threshold: 5, minViews: 10000 }); }); -test('normalizeSettings trims the key and coerces numeric strings', () => { - assert.deepEqual(normalizeSettings({ apiKey: ' abc123 ', threshold: '7.5', minViews: '20000' }), { - apiKey: 'abc123', - threshold: 7.5, - minViews: 20000, - }); +test('normalizeSettings coerces numeric strings', () => { + assert.deepEqual(normalizeSettings({ threshold: '7.5', minViews: '20000' }), { threshold: 7.5, minViews: 20000 }); }); test('normalizeSettings replaces invalid thresholds with the default', () => { @@ -30,6 +26,6 @@ test('normalizeSettings replaces invalid minViews with the default and floors de assert.equal(normalizeSettings({ minViews: 0 }).minViews, 0); }); -test('normalizeSettings ignores a non-string key', () => { - assert.equal(normalizeSettings({ apiKey: 12345 }).apiKey, ''); +test('normalizeSettings drops fields it does not know (the old apiKey)', () => { + assert.deepEqual(normalizeSettings({ apiKey: 'secret', threshold: 6 }), { threshold: 6, minViews: 10000 }); });