Skip to content

Instantly share code, notes, and snippets.

@bitbybit
Created July 9, 2026 20:53
Show Gist options
  • Select an option

  • Save bitbybit/70a494c081f597900f04a1277b207289 to your computer and use it in GitHub Desktop.

Select an option

Save bitbybit/70a494c081f597900f04a1277b207289 to your computer and use it in GitHub Desktop.
Convert Sofa export into the Listy JSON format

Before running the script, initialize a project and install the required dependencies:

npm init -y
npm install csv-parse
npm install -D typescript tsx @types/node

convert.ts should be placed in the same directory as items.csv

Run:

npx tsx convert.ts
import * as fs from 'fs';
import { parse } from 'csv-parse/sync';
interface SofaItem {
id: string;
list_name: string;
type: string;
api_id: string;
api_source: string;
external_link: string;
image_url: string;
title: string;
item_description: string;
date_added: string;
item_status: string;
}
function formatListyDate(isoString: string | undefined): string {
if (!isoString) return '';
const date = new Date(isoString);
if (isNaN(date.getTime())) return '';
const pad = (n: number) => n.toString().padStart(2, '0');
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()}, ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function deriveUrl(item: SofaItem): string {
if (item.external_link) return item.external_link;
if (item.api_source === 'tmdb' && item.api_id) {
return `https://www.themoviedb.org/${item.type === 'tv' ? 'tv' : 'movie'}/${item.api_id}`;
}
return '';
}
function convertSofaToListy(csvFilePath: string, jsonFilePath: string) {
console.log(`Reading CSV from ${csvFilePath}...`);
// Read and parse the CSV
const csvData = fs.readFileSync(csvFilePath, 'utf-8');
const records: SofaItem[] = parse(csvData, {
columns: true,
skip_empty_lines: true,
relax_quotes: true
});
// Group items by their list_name
const listsMap = new Map<string, SofaItem[]>();
for (const record of records) {
const listName = record.list_name || 'Uncategorized';
if (!listsMap.has(listName)) {
listsMap.set(listName, []);
}
listsMap.get(listName)!.push(record);
}
const currentDateFormatted = formatListyDate(new Date().toISOString());
let customOrderIndex = 0;
// Map to Listy's structure
const listyLists = Array.from(listsMap.entries()).map(([listName, items]) => {
const listyItems = items.map(item => {
const attributes = [
{ key: "RATING", value: "0.0" } // Defaulting to 0.0
];
if (item.image_url) {
attributes.push({ key: "COVER", value: item.image_url });
}
if (item.item_description) {
// Remove extra whitespace/newlines from descriptions
attributes.push({ key: "DESCRIPTION", value: item.item_description.trim() });
}
const url = deriveUrl(item);
if (url) {
try {
const urlObj = new URL(url);
attributes.push({
key: "FAVICON",
value: `https://www.google.com/s2/favicons?sz=32&domain=${urlObj.hostname}`
});
} catch (e) {
// Fail silently on bad URLs
}
}
return {
type: "Generic", // Listy sets individual items to 'Generic'
url: url,
title: item.title,
attributes: attributes,
marked: item.item_status === 'completed',
dateAdded: formatListyDate(item.date_added) || currentDateFormatted
};
});
// Determine list creation date from the oldest item in that list
const listCreateDate = items[0]?.date_added ? formatListyDate(items[0].date_added) : currentDateFormatted;
customOrderIndex++;
return {
title: listName,
type: "Movies",
items: listyItems,
update: currentDateFormatted,
customOrderIndex: customOrderIndex,
icon: "film",
create: listCreateDate,
needFetchAfterImport: false
};
});
const listyExport = {
appVersion: "1.7.2",
engine_version: 26,
lists: listyLists,
appBuild: "20260330",
appPlatform: "MacOS",
appName: "Listy",
appWebSite: "https://listy.is",
date_export: currentDateFormatted
};
fs.writeFileSync(jsonFilePath, JSON.stringify(listyExport, null, 2));
console.log(`✅ Successfully converted ${records.length} items across ${listyLists.length} lists.`);
console.log(`Saved output to ${jsonFilePath}`);
}
convertSofaToListy('items.csv', 'ItemsToImport.listy');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment