Created
June 15, 2025 15:58
-
-
Save mary-ext/797b3ece51e67dcb3f741ce43445d616 to your computer and use it in GitHub Desktop.
kenmei export to mihon/tachiyomi backup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { assert } from '@std/assert'; | |
import * as p from '@mary/protobuf'; | |
const Source = p.message({ | |
id: p.int64(), | |
name: p.string(), | |
}, { | |
id: 2, | |
name: 1, | |
}); | |
const Category = p.message({ | |
id: p.int64(), | |
order: p.optional(p.int64(), 0n), | |
name: p.string(), | |
}, { | |
id: 3, | |
order: 2, | |
name: 1, | |
}); | |
type Category = p.InferInput<typeof Category>; | |
const Chapter = p.message({ | |
url: p.string(), | |
chapterNumber: p.optional(p.float(), 0), | |
name: p.string(), | |
read: p.optional(p.boolean(), false), | |
}, { | |
url: 1, | |
chapterNumber: 9, | |
name: 2, | |
read: 4, | |
}); | |
type Chapter = p.InferInput<typeof Chapter>; | |
const Manga = p.message({ | |
source: p.int64(), | |
url: p.string(), | |
title: p.string(), | |
chapters: p.optional(p.repeated(Chapter), []), | |
categories: p.optional(p.repeated(p.int64()), []), | |
}, { | |
source: 1, | |
url: 2, | |
title: 3, | |
chapters: 16, | |
categories: 17, | |
}); | |
type Manga = p.InferInput<typeof Manga>; | |
const Backup = p.message({ | |
entries: p.repeated(Manga), | |
categories: p.optional(p.repeated(Category), []), | |
sources: p.optional(p.repeated(Source), []), | |
}, { | |
entries: 1, | |
categories: 2, | |
sources: 101, | |
}); | |
{ | |
const buffer = Deno.readFileSync('./backup.tachibk'); | |
const decoded = p.decode(Backup, buffer); | |
console.log(decoded); | |
} | |
{ | |
interface RawEntry { | |
title: string; | |
status: 'reading' | 'on_hold' | 'plan_to_read' | 'completed' | 'dropped'; | |
score: number | ''; | |
last_volume_read: number | ''; | |
last_chapter_read: number | ''; | |
tracked_site: 'MangaDex' | 'Comick' | 'LHTranslation'; | |
series_url: string; | |
tags: string; | |
} | |
const READING_CATEGORY: Category = { id: 1n, order: 0n, name: `Reading` } as const; | |
const PAUSED_CATEGORY: Category = { id: 2n, order: 1n, name: `Paused` } as const; | |
const PLANNED_CATEGORY: Category = { id: 3n, order: 2n, name: `Planned` } as const; | |
const COMPLETED_CATEGORY: Category = { id: 4n, order: 3n, name: `Completed` } as const; | |
const DROPPED_CATEGORY: Category = { id: 5n, order: 4n, name: `Dropped` } as const; | |
const NSFW_CATEGORY: Category = { id: 6n, order: 5n, name: `NSFW` } as const; | |
const rawEntries: RawEntry[] = JSON.parse(Deno.readTextFileSync('./kenmei.json')); | |
const MANGADEX_ID = 2499283573021220255n; | |
const COMICK_ID = 2971557565147974499n; | |
const LHTRANSLATION_ID = 8802607595629671202n; | |
const toFloat32 = (value: number): number => { | |
return new Float32Array([value])[0]; | |
}; | |
const backup: p.InferInput<typeof Backup> = { | |
categories: [ | |
READING_CATEGORY, | |
PAUSED_CATEGORY, | |
PLANNED_CATEGORY, | |
COMPLETED_CATEGORY, | |
DROPPED_CATEGORY, | |
NSFW_CATEGORY, | |
], | |
sources: [ | |
{ name: 'MangaDex', id: MANGADEX_ID }, | |
{ name: 'Comick', id: COMICK_ID }, | |
], | |
entries: rawEntries.map((entry): Manga => { | |
const categories: Category[] = []; | |
switch (entry.status) { | |
case 'completed': { | |
categories.push(COMPLETED_CATEGORY); | |
break; | |
} | |
case 'dropped': { | |
categories.push(DROPPED_CATEGORY); | |
break; | |
} | |
case 'on_hold': { | |
categories.push(PAUSED_CATEGORY); | |
break; | |
} | |
case 'plan_to_read': { | |
categories.push(PLANNED_CATEGORY); | |
break; | |
} | |
case 'reading': { | |
categories.push(READING_CATEGORY); | |
break; | |
} | |
} | |
if (entry.tags === 'NSFW') { | |
categories.push(NSFW_CATEGORY); | |
} | |
const chapters: Chapter[] = []; | |
if (entry.last_chapter_read) { | |
const last = entry.last_chapter_read; | |
const lastDecimal = Math.floor(last * 10); | |
for (let i = 0; i <= lastDecimal; i++) { | |
const num = i / 10; | |
const floated = toFloat32(num); | |
chapters.push({ | |
url: `unknown_${num}`, | |
name: `Ch. ${num}: Refresh your library`, | |
chapterNumber: num, | |
read: true, | |
}); | |
if (num !== floated) { | |
chapters.push({ | |
url: `unknown_${floated}`, | |
name: `Ch. ${floated}: Refresh your library`, | |
chapterNumber: floated, | |
read: true, | |
}); | |
} | |
} | |
// Handle cases where last chapter has finer precision than 0.1 | |
if (last * 10 !== lastDecimal) { | |
chapters.push({ | |
url: `unknown_${last}`, | |
name: `Ch. ${last}: Refresh your library`, | |
chapterNumber: toFloat32(last), | |
read: true, | |
}); | |
} | |
chapters.reverse(); | |
} | |
switch (entry.tracked_site) { | |
case 'MangaDex': { | |
return { | |
source: MANGADEX_ID, | |
title: entry.title, | |
url: entry.series_url.replace('https://mangadex.org/title/', '/manga/'), | |
categories: categories.map((category) => category.order!), | |
chapters: chapters, | |
}; | |
} | |
case 'Comick': { | |
return { | |
source: COMICK_ID, | |
title: entry.title, | |
url: entry.series_url.replace('https://comick.io/comic/', '/comic/') + '#', | |
categories: categories.map((category) => category.order!), | |
chapters: chapters, | |
}; | |
} | |
case 'LHTranslation': { | |
return { | |
source: LHTRANSLATION_ID, | |
title: entry.title, | |
url: entry.series_url.replace('https://lhtranslation.net/manga/', '/manga/'), | |
categories: categories.map((category) => category.order!), | |
chapters: chapters, | |
}; | |
} | |
default: { | |
assert(false, `unknown tracked site ${entry.tracked_site}`); | |
} | |
} | |
}), | |
}; | |
console.log(backup); | |
Deno.writeFileSync('./kenmei.tachibk', p.encode(Backup, backup)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment