feat: initial commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.zed/
|
||||||
|
.prettierc.yml
|
||||||
|
.oxlintrc.json
|
||||||
|
.env
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
143
.oxlintrc.json
Normal file
143
.oxlintrc.json
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["oxc", "unicorn", "typescript", "import", "node", "promise"],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error",
|
||||||
|
"suspicious": "error",
|
||||||
|
"pedantic": "warn",
|
||||||
|
"perf": "warn"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
// It's a TypeScript project, it should use ECMA imports
|
||||||
|
"import/no-amd": "error",
|
||||||
|
"import/no-commonjs": "error",
|
||||||
|
|
||||||
|
// Restrict dangerous practices
|
||||||
|
"eslint/eqeqeq": ["error", "always", { "null": "ignore" }],
|
||||||
|
"default-case": "error",
|
||||||
|
"no-bitwise": "error",
|
||||||
|
"no-console": "off",
|
||||||
|
"no-empty": "error",
|
||||||
|
"no-empty-function": "error",
|
||||||
|
"no-iterator": "error",
|
||||||
|
"no-plusplus": "error",
|
||||||
|
"no-proto": "error",
|
||||||
|
"no-regex-spaces": "error",
|
||||||
|
"no-restricted-globals": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"no-void": "error",
|
||||||
|
"no-new-require": "error",
|
||||||
|
"no-const-enum": "error",
|
||||||
|
"import/no-cycle": "error",
|
||||||
|
"import/no-dynamic-require": "error",
|
||||||
|
"promise/catch-or-return": "error",
|
||||||
|
"promise/spec-only": "error",
|
||||||
|
"typescript/no-dynamic-delete": "error",
|
||||||
|
"typescript/no-empty-object-type": "error",
|
||||||
|
"typescript/no-explicit-any": "error",
|
||||||
|
"unicorn/no-abusive-eslint-disable": "error",
|
||||||
|
"unicorn/no-length-as-slice-end": "error",
|
||||||
|
"unicorn/prefer-number-properties": "error",
|
||||||
|
"typescript/no-import-type-side-effects": "error",
|
||||||
|
"unicorn/prefer-event-target": "off",
|
||||||
|
"unicorn/prefer-add-event-listener": "off",
|
||||||
|
|
||||||
|
// Enforce browser rules
|
||||||
|
"no-alert": "error",
|
||||||
|
"no-script-url": "error",
|
||||||
|
"unicorn/no-document-cookie": "error",
|
||||||
|
|
||||||
|
// Enforce styling
|
||||||
|
"arrow-body-style": ["error", "as-needed"],
|
||||||
|
"curly": "error",
|
||||||
|
"default-case-last": "error",
|
||||||
|
"default-param-last": "error",
|
||||||
|
"func-name": ["error", "as-needed"],
|
||||||
|
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||||
|
"grouped-accessor-pair": ["error", "getBeforeSet"],
|
||||||
|
"id-length": ["error", { "exceptions": ["_", "t", "z"] }],
|
||||||
|
"max-params": "warn",
|
||||||
|
"new-cap": ["error", { "newIsCap": true, "capIsNew": true }],
|
||||||
|
"no-duplicate-import": "error",
|
||||||
|
"no-extra-label": "error",
|
||||||
|
"no-label-var": "error",
|
||||||
|
"no-labels": "error",
|
||||||
|
"no-lone-blocks": "error",
|
||||||
|
"no-magic-number": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"ignore": [0, 1, 2],
|
||||||
|
"ignoreDefaultValues": true,
|
||||||
|
"ignoreClassFieldInitialValues": true,
|
||||||
|
"ignoreEnums": true,
|
||||||
|
"ignoreNumericLiteralTypes": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-multi-assign": "error",
|
||||||
|
"no-nested-ternary": "error",
|
||||||
|
"no-new-func": "error",
|
||||||
|
"no-return-assign": "error",
|
||||||
|
"no-template-curly-in-string": "error",
|
||||||
|
"operator-assignment": ["error", "always"],
|
||||||
|
"prefer-exponentiation-operator": "error",
|
||||||
|
"prefer-object-spread": "error",
|
||||||
|
"prefer-promise-reject-errors": "error",
|
||||||
|
"prefer-rest-params": "error",
|
||||||
|
"prefer-spread": "error",
|
||||||
|
"import/first": ["error", "absolute-first"],
|
||||||
|
"import/no-anonymous-default-export": "error",
|
||||||
|
"import/no-duplicates": "error",
|
||||||
|
"import/no-mutable-exports": "warn",
|
||||||
|
"import/no-named-default": "error",
|
||||||
|
"import/no-namespace": "warn",
|
||||||
|
"import/no-default-export": "warn", // TODO: move to error once migrated
|
||||||
|
"promise/avoid-new": "warn", // Maybe error ?
|
||||||
|
"promise/no-nesting": "error",
|
||||||
|
"promise/no-return-wrap": "error",
|
||||||
|
"promise/param-names": "error",
|
||||||
|
"promise/prefer-await-to-callbacks": "warn", // Maybe error ?
|
||||||
|
"promise/prefer-await-to-then": "error", // Maybe error ?
|
||||||
|
"promise/prefer-catch": "error",
|
||||||
|
"typescript/adjacent-overload-signatures": "error",
|
||||||
|
"typescript/array-type": ["error", { "default": "array" }],
|
||||||
|
"typescript/consistent-generic-constructors": "error",
|
||||||
|
"typescript/consistent-indexed-object-style": ["error", "record"],
|
||||||
|
"typescript/consistent-type-definitions": ["error", "type"],
|
||||||
|
"typescript/consistent-type-imports": "error",
|
||||||
|
"typescript/no-empty-interface": "error",
|
||||||
|
"typescript/no-inferrable-types": "warn", // Maybe error ?
|
||||||
|
"typescript/prefer-for-of": "error",
|
||||||
|
"typescript/prefer-function-type": "error",
|
||||||
|
"typescript/no-non-null-asserted-nullish-coalescing": "error",
|
||||||
|
"unicorn/error-message": "error",
|
||||||
|
"unicorn/no-array-method-this-argument": "error",
|
||||||
|
"unicorn/no-await-expression-member": "error",
|
||||||
|
"unicorn/no-console-spaces": "error",
|
||||||
|
"unicorn/no-zero-fractions": "error",
|
||||||
|
"unicorn/number-literal-case": "error",
|
||||||
|
"unicorn/prefer-array-flat-map": "error",
|
||||||
|
"unicorn/prefer-logical-operator-over-ternary": "error",
|
||||||
|
"unicorn/no-array-for-each": "error",
|
||||||
|
"unicorn/no-array-reduce": "warn",
|
||||||
|
"unicorn/no-magic-array-flat-depth": "error",
|
||||||
|
"unicorn/no-process-exit": "error",
|
||||||
|
"unicorn/prefer-modern-math-apis": "error",
|
||||||
|
"unicorn/prefer-node-protocol": "error",
|
||||||
|
"typescript/explicit-function-return-type": "warn"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["src/composables/**/*.ts", "src/utils/**/*.ts", "*.config.ts"],
|
||||||
|
"rules": {
|
||||||
|
"no-default-export": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["src/stores/**/*.ts", "src/composables/**/*.ts"],
|
||||||
|
"rules": {
|
||||||
|
"max-lines-per-function": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
.prettierrc.yml
Normal file
4
.prettierrc.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
trailingComma: es5
|
||||||
|
tabWidth: 2
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
23
.zed/settings.json
Normal file
23
.zed/settings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"lsp": {
|
||||||
|
"deno": {
|
||||||
|
"settings": {
|
||||||
|
"deno": {
|
||||||
|
"enable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"TypeScript": {
|
||||||
|
"language_servers": [
|
||||||
|
"deno",
|
||||||
|
"!typescript-language-server",
|
||||||
|
"!vtsls",
|
||||||
|
"!eslint",
|
||||||
|
"..."
|
||||||
|
],
|
||||||
|
"formatter": "prettier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM denoland/deno:2.6.3-alpine
|
||||||
|
|
||||||
|
# Create working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN deno install
|
||||||
|
|
||||||
|
# Prepare flags to run script
|
||||||
|
ENTRYPOINT ["deno", "run", "-P"]
|
||||||
76
bin/reorder-all-series.ts
Normal file
76
bin/reorder-all-series.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
fetchLibraryMangas,
|
||||||
|
fetchMangaChapters,
|
||||||
|
updateMetadata,
|
||||||
|
} from '~/lib/komga.ts';
|
||||||
|
|
||||||
|
const getChapterSort = (chapter: any) => {
|
||||||
|
const volumeNumber = /vol(?:ume)?.?([0-9]+)/i.exec(chapter.name)?.[1] || '1';
|
||||||
|
|
||||||
|
const chapterNumber =
|
||||||
|
/ch(?:apter)?.?([0-9.]+)/i.exec(chapter.name)?.[1] || '';
|
||||||
|
const chapterFloat = Number.parseFloat(chapterNumber);
|
||||||
|
|
||||||
|
if (!chapterNumber || Number.isNaN(chapterFloat)) {
|
||||||
|
throw new Error(
|
||||||
|
`Ch. ${chapterNumber} cannot be parsed from ${chapter.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `Ch. ${chapterNumber} - Vol. ${volumeNumber}`,
|
||||||
|
numberSort: chapterFloat,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitiseMetadata = (chapter: any, chaptersSeen: Set<number>) => {
|
||||||
|
const data = getChapterSort(chapter);
|
||||||
|
|
||||||
|
const isDuplicate = chaptersSeen.has(data.numberSort);
|
||||||
|
if (isDuplicate) {
|
||||||
|
console.log(`⚠️ Ch. ${data.numberSort} already seen, marking as duplicate`);
|
||||||
|
}
|
||||||
|
|
||||||
|
chaptersSeen.add(data.numberSort);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
numberSortLock: true,
|
||||||
|
tags: isDuplicate
|
||||||
|
? [...chapter.metadata.tags, 'oxy.DUPLICATE']
|
||||||
|
: chapter.metadata.tags.filter((tag: string) => tag !== 'oxy.DUPLICATE'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`== Sorting library ${Deno.env.get('LIBRARY_ID')} (${new Date().toISOString()}) ==`
|
||||||
|
);
|
||||||
|
for (const manga of await fetchLibraryMangas(Deno.env.get('LIBRARY_ID'))) {
|
||||||
|
console.log(`Sorting ${manga.name} (${manga.booksCount} chapters)...`);
|
||||||
|
console.group();
|
||||||
|
|
||||||
|
const chapters = await fetchMangaChapters(manga.id);
|
||||||
|
|
||||||
|
const chaptersSeen = new Set<number>();
|
||||||
|
const metadata = new Map(
|
||||||
|
chapters.map((chap) => [chap.id, sanitiseMetadata(chap, chaptersSeen)])
|
||||||
|
);
|
||||||
|
const updates = [...metadata].toSorted(
|
||||||
|
([, a], [, b]) => a.numberSort - b.numberSort
|
||||||
|
);
|
||||||
|
|
||||||
|
let updateCount = 0;
|
||||||
|
for (const [id, metadata] of updates) {
|
||||||
|
await updateMetadata(id, {
|
||||||
|
...metadata,
|
||||||
|
number: updateCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✔ Updated ${updateCount} chapters !`);
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
19
deno.json
Normal file
19
deno.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json",
|
||||||
|
|
||||||
|
"tasks": {
|
||||||
|
"format": "deno x prettier --write .",
|
||||||
|
"lint": "deno x oxlint --fix"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"~": "./",
|
||||||
|
|
||||||
|
"ofetch": "npm:ofetch@^1.5.1"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"default": {
|
||||||
|
"env": true,
|
||||||
|
"net": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
deno.lock
generated
Normal file
30
deno.lock
generated
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"specifiers": {
|
||||||
|
"npm:ofetch@^1.5.1": "1.5.1"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"destr@2.0.5": {
|
||||||
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="
|
||||||
|
},
|
||||||
|
"node-fetch-native@1.6.7": {
|
||||||
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="
|
||||||
|
},
|
||||||
|
"ofetch@1.5.1": {
|
||||||
|
"integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==",
|
||||||
|
"dependencies": [
|
||||||
|
"destr",
|
||||||
|
"node-fetch-native",
|
||||||
|
"ufo"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ufo@1.6.1": {
|
||||||
|
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:ofetch@^1.5.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/komga.ts
Normal file
59
lib/komga.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ofetch } from 'ofetch';
|
||||||
|
|
||||||
|
export const komga = ofetch.create({
|
||||||
|
baseURL: new URL('/api', Deno.env.get('BASE_URL')!).href,
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': Deno.env.get('API_KEY')!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchLibraryMangas = async (
|
||||||
|
libraryId: string
|
||||||
|
): Promise<Record<string, any>[]> => {
|
||||||
|
const { content } = await komga('/v1/series/list', {
|
||||||
|
method: 'POST',
|
||||||
|
query: {
|
||||||
|
unpaged: true,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
condition: {
|
||||||
|
libraryId: {
|
||||||
|
operator: 'is',
|
||||||
|
value: libraryId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMangaChapters = async (
|
||||||
|
seriesId: string
|
||||||
|
): Promise<Record<string, any>[]> => {
|
||||||
|
const { content } = await komga('/v1/books/list', {
|
||||||
|
method: 'POST',
|
||||||
|
query: {
|
||||||
|
unpaged: true,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
condition: {
|
||||||
|
seriesId: {
|
||||||
|
operator: 'is',
|
||||||
|
value: seriesId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMetadata = (
|
||||||
|
id: string,
|
||||||
|
body: Record<string, unknown>
|
||||||
|
): Promise<void> =>
|
||||||
|
komga(`/v1/books/${id}/metadata`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user