feat(komga): add script to make a Series from a Read List

This commit is contained in:
matt1432 2025-03-24 13:18:27 -04:00
parent fb75377969
commit 4cdd8c0d43
14 changed files with 2076 additions and 3 deletions

View file

@ -8,6 +8,7 @@ This directory contains every derivations for apps exposed by this flake.
| ---- | ----------- |
| `extract-subs` | Extract all `srt` subtitle files from a `mkv` video with the appropriate name. |
| `gen-docs` | Generates the READMEs in this repository from nix attributes. |
| `list2series` | Converts a Komga read list into a comics series for reading with mihon. |
| `mc-mods` | Checks if a list of mods have a version available for a specific Minecraft version and a specific loader. |
| `pin-inputs` | Takes a list of inputs to pin to their current rev in `flake.lock`. |
| `update-sources` | Updates all derivation sources in this repository and generates a commit message for the changes made. |

3
apps/list2series/.envrc Normal file
View file

@ -0,0 +1,3 @@
use flake $FLAKE#node
(cd ../config; npm ci)
npm ci

1
apps/list2series/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

View file

@ -0,0 +1,11 @@
{buildApp, ...}:
buildApp {
src = ./.;
npmDepsHash = "sha256-GrGSXKAH8w068rIOFwmQoM1Zn68ESkazBiDaoE0mrQw=";
runtimeInputs = [];
meta.description = ''
Converts a Komga read list into a comics series for reading with mihon.
'';
}

View file

@ -0,0 +1,3 @@
import eslintConf from 'eslint-conf';
export default eslintConf;

1812
apps/list2series/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
{
"name": "list2series",
"version": "0.0.0",
"bin": "out/bin/app.cjs",
"type": "module",
"scripts": {
"build": "node_ver=$(node -v); esbuild src/app.ts --bundle --platform=node --target=\"node${node_ver:1:2}\" --outfile=out/bin/app.cjs"
},
"dependencies": {
"@types/node": "22.13.11",
"axios": "^1.8.4",
"esbuild": "0.25.1",
"eslint": "9.23.0",
"jiti": "2.4.2",
"pkg-types": "2.1.0",
"typescript": "5.8.2"
},
"devDependencies": {
"eslint-conf": "file:../config"
}
}

204
apps/list2series/src/app.ts Normal file
View file

@ -0,0 +1,204 @@
import axios from 'axios';
import { copyFileSync, mkdirSync, readFileSync, rmSync } from 'fs';
import { basename } from 'path';
// eslint-disable-next-line
type Book = any;
const API = JSON.parse(
readFileSync(`${process.env.FLAKE}/apps/list2series/.env`, { encoding: 'utf-8' }),
).API;
// Examples of calling this script:
// $ just l2s copy 0K65Q482KK7SD
// $ just l2s meta 0K65Q482KK7SD
const LIST_ID = process.argv[3];
const getListInfo = async() => {
const res = await axios.request({
method: 'get',
maxBodyLength: Infinity,
url: `https://komga.nelim.org/api/v1/readlists/${LIST_ID}`,
headers: {
'Accept': 'application/json',
'X-API-Key': API,
},
});
return res.data;
};
const getSeriesBooks = async(listName: string, seriesPath: string): Promise<Book[]> => {
const series = await axios.request({
method: 'post',
maxBodyLength: Infinity,
url: 'https://komga.nelim.org/api/v1/series/list?unpaged=true',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-Key': API,
},
data: JSON.stringify({
condition: {
title: {
operator: 'isNot',
value: '',
},
},
}),
});
const seriesId = (series.data.content as Book[]).find((s) => s.url === seriesPath).id;
// Reset Series metadata
axios.request({
method: 'patch',
maxBodyLength: Infinity,
url: `https://komga.nelim.org/api/v1/series/${seriesId}/metadata`,
headers: {
'Content-Type': 'application/json',
'X-API-Key': API,
},
data: JSON.stringify({
ageRating: null,
ageRatingLock: true,
alternateTitles: null,
alternateTitlesLock: true,
genres: null,
genresLock: true,
language: null,
languageLock: true,
links: null,
linksLock: true,
publisherLock: true,
readingDirection: 'LEFT_TO_RIGHT',
readingDirectionLock: true,
sharingLabels: null,
sharingLabelsLock: true,
status: null,
statusLock: true,
summary: null,
summaryLock: true,
tags: null,
tagsLock: true,
title: listName,
titleLock: true,
titleSort: listName,
titleSortLock: true,
totalBookCountLock: true,
}),
});
const books = await axios.request({
method: 'post',
maxBodyLength: Infinity,
url: 'https://komga.nelim.org/api/v1/books/list?unpaged=true',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-Key': API,
},
data: JSON.stringify({
condition: {
seriesId: {
operator: 'is',
value: seriesId,
},
},
}),
});
return books.data.content;
};
const getBookInfo = async(id: string) => {
const res = await axios.request({
method: 'get',
maxBodyLength: Infinity,
url: `https://komga.nelim.org/api/v1/books/${id}`,
headers: {
'Accept': 'application/json',
'X-API-Key': API,
},
});
return res.data;
};
const scanLibrary = async() => {
return await axios.request({
method: 'post',
maxBodyLength: Infinity,
url: 'https://komga.nelim.org/api/v1/libraries/0K4QG58XA29DZ/scan',
headers: {
'X-API-Key': API,
},
});
};
const setBookMetadata = async(i: number, source: Book, target: Book) => {
source.metadata.title = `${source.seriesTitle} Issue #${source.number}`;
source.metadata.number = i.toString();
source.metadata.numberSort = i;
const metadata = JSON.stringify(source.metadata);
const res = await axios.request({
method: 'patch',
maxBodyLength: Infinity,
url: `https://komga.nelim.org/api/v1/books/${target.id}/metadata`,
headers: {
'Content-Type': 'application/json',
'X-API-Key': API,
},
data: metadata,
});
return res;
};
const main = async() => {
const list = await getListInfo();
const ids = list.bookIds as string[];
const seriesPath = `/data/comics/[List] ${list.name}`;
const listBooks = [] as Book[];
for (let i = 0; i < ids.length; i++) {
const book = await getBookInfo(ids[i]);
listBooks[i] = book;
};
if (process.argv[2] === 'copy') {
rmSync(seriesPath, { recursive: true, force: true });
mkdirSync(seriesPath, { recursive: true });
for (const book of listBooks) {
const bookPath = book.url;
const inListPath = `${seriesPath}/${basename(bookPath)}`;
console.log(`copying ${basename(bookPath)}`);
copyFileSync(bookPath, inListPath);
}
await scanLibrary();
}
else if (process.argv[2] === 'meta') {
const seriesBooks = await getSeriesBooks(`[List] ${list.name}`, seriesPath);
for (const target of seriesBooks) {
const source = listBooks.find((b) => basename(b.url) === basename(target.url));
if (source) {
const i = listBooks.indexOf(source) + 1;
console.log(`Setting metadata for ${source.name}`);
setBookMetadata(i, source, target);
}
}
}
};
main();

View file

@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../config/tsconfig.base.json",
"includes": [
"*.ts",
"**/*.ts",
"*.js",
"**/*.js"
]
}

View file

@ -8,6 +8,7 @@
listToAttrs (map (x: nameValuePair x (callPackage ./${x})) [
"extract-subs"
"gen-docs"
"list2series"
"mc-mods"
"pin-inputs"
"update-sources"