mirror of
https://github.com/BillyOutlast/metadata-scrapers.git
synced 2026-02-04 03:01:16 +01:00
init
This commit is contained in:
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
* text=auto
|
||||
|
||||
*.yml eol=lf diff=yaml linguist-detectable
|
||||
*.py eol=lf diff=python
|
||||
*.md eol=lf diff=markdown
|
||||
35
.github/workflows/deploy.yml
vendored
Normal file
35
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Deploy repository to Github Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
- run: |
|
||||
./build_site.sh _site/${{ github.ref_name }}
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
16
.github/workflows/validate.yml
vendored
Normal file
16
.github/workflows/validate.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Validate Scrapers
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
- run: cd ./validator && yarn install --frozen-lockfile
|
||||
- run: node ./validate.js --ci
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"validator/scraper.schema.json": "*.yml"
|
||||
},
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
}
|
||||
42
README.md
42
README.md
@@ -1 +1,41 @@
|
||||
# metadata-scrapers
|
||||
# FansDB community metadata scrapers
|
||||
|
||||
These community scrapers are tailored towards FansDB data structure. To use you need to have [stashapp/stash](https://github.com/stashapp/stash) application.
|
||||
|
||||
## Install
|
||||
|
||||
1. To add a new source go to Settings > **Metadata Providers** page.
|
||||
1. Under Available Scrapers click **Add Source**.
|
||||
1. Add the following details:
|
||||
- Name: `FansDB`
|
||||
- Source URL: `https://fansdb.github.io/scrapers/main/index.yml`
|
||||
- Local path: `fansdb`
|
||||
1. Click Confirm.
|
||||
1. Under Available Scrapers select **FansDB** package to expand it and see all available scrapers.
|
||||
1. Click the checkmark next to relevant scraper and click **Insall**.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome.
|
||||
|
||||
### Repository structure
|
||||
|
||||
- Each scraper should have its own folder under [./scrapers](./scrapers).
|
||||
- Folder name should reflect the network name used in FansDB.
|
||||
|
||||
### Validation
|
||||
The scrapers in this repository can be validated against a schema and checked for common errors.
|
||||
|
||||
First, install the validator's dependencies - inside the [`./validator`](./validator) folder, run: `yarn`.
|
||||
|
||||
Then, to run the validator, use `node validate.js` in the root of the repository.
|
||||
Specific scrapers can be checked using: `node validate.js scrapers/folder/example.yml scrapers/folder2/example2.yml`
|
||||
|
||||
## Attributions
|
||||
|
||||
From [https://github.com/stashapp/CommunityScrapers](https://github.com/stashapp/CommunityScrapers) under AGPL-3.0:
|
||||
|
||||
- `./validate.js`
|
||||
- `./build-site.sh`
|
||||
- `./validator/*`
|
||||
- `./.github/workflows/validate.yml`
|
||||
89
build_site.sh
Normal file
89
build_site.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# builds a repository of scrapers
|
||||
# outputs to _site with the following structure:
|
||||
# index.yml
|
||||
# <scraper_id>.zip
|
||||
# Each zip file contains the scraper.yml file and any other files in the same directory
|
||||
|
||||
outdir="$1"
|
||||
if [ -z "$outdir" ]; then
|
||||
outdir="_site"
|
||||
fi
|
||||
|
||||
rm -rf "$outdir"
|
||||
mkdir -p "$outdir"
|
||||
|
||||
buildScraper()
|
||||
{
|
||||
f=$1
|
||||
dir=$(dirname "$f")
|
||||
|
||||
# get the scraper id from the filename
|
||||
scraper_id=$(basename "$f" .yml)
|
||||
versionFile=$f
|
||||
if [ "$scraper_id" == "package" ]; then
|
||||
scraper_id=$(basename "$dir")
|
||||
fi
|
||||
|
||||
if [ "$dir" != "./scrapers" ]; then
|
||||
versionFile="$dir"
|
||||
fi
|
||||
|
||||
echo "Processing $scraper_id"
|
||||
|
||||
# create a directory for the version
|
||||
version=$(git log -n 1 --pretty=format:%h -- "$versionFile")
|
||||
updated=$(TZ=UTC0 git log -n 1 --date="format-local:%F %T" --pretty=format:%ad -- "$versionFile")
|
||||
|
||||
# create the zip file
|
||||
# copy other files
|
||||
zipfile=$(realpath "$outdir/$scraper_id.zip")
|
||||
|
||||
name=$(grep "^name:" "$f" | cut -d' ' -f2- | sed -e 's/\r//' -e 's/^"\(.*\)"$/\1/')
|
||||
ignore=$(grep "^# ignore:" "$f" | cut -c 10- | sed -e 's/\r//')
|
||||
dep=$(grep "^# requires:" "$f" | cut -c 12- | sed -e 's/\r//')
|
||||
|
||||
# always ignore package file
|
||||
ignore="-x $ignore package"
|
||||
|
||||
pushd "$dir" > /dev/null
|
||||
if [ "$dir" != "./scrapers" ]; then
|
||||
zip -r "$zipfile" . ${ignore} > /dev/null
|
||||
else
|
||||
zip "$zipfile" "$scraper_id.yml" > /dev/null
|
||||
fi
|
||||
popd > /dev/null
|
||||
|
||||
# write to spec index
|
||||
echo "- id: $scraper_id
|
||||
name: $name
|
||||
version: $version
|
||||
date: $updated
|
||||
path: $scraper_id.zip
|
||||
sha256: $(sha256sum "$zipfile" | cut -d' ' -f1)" >> "$outdir"/index.yml
|
||||
|
||||
# handle dependencies
|
||||
if [ ! -z "$dep" ]; then
|
||||
echo " requires:" >> "$outdir"/index.yml
|
||||
for d in ${dep//,/ }; do
|
||||
echo " - $d" >> "$outdir"/index.yml
|
||||
done
|
||||
fi
|
||||
|
||||
echo "" >> "$outdir"/index.yml
|
||||
}
|
||||
|
||||
# find all yml files in ./scrapers - these are packages individually
|
||||
#for f in ./scrapers/*.yml; do
|
||||
# buildScraper "$f"
|
||||
#done
|
||||
|
||||
find ./scrapers/ -mindepth 2 -name *.yml -print0 | while read -d $'\0' f; do
|
||||
buildScraper "$f"
|
||||
done
|
||||
|
||||
# handle dependency packages
|
||||
find ./scrapers/ -mindepth 2 -name package -print0 | while read -d $'\0' f; do
|
||||
buildScraper "$f"
|
||||
done
|
||||
3
validate.js
Normal file
3
validate.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
require('./validator/index.js')();
|
||||
357
validator/index.js
Normal file
357
validator/index.js
Normal file
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const safeRequire = (name) => {
|
||||
try {
|
||||
return require(name);
|
||||
} catch (error) {
|
||||
if (error && error.code === 'MODULE_NOT_FOUND') {
|
||||
console.log(`Error: Cannot find module '${name}', have you installed the dependencies?`);
|
||||
process.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const Ajv = safeRequire('ajv').default;
|
||||
const betterAjvErrors = safeRequire('better-ajv-errors').default;
|
||||
const chalk = safeRequire('chalk');
|
||||
const YAML = safeRequire('yaml');
|
||||
const addFormats = safeRequire('ajv-formats');
|
||||
|
||||
// https://www.peterbe.com/plog/nodejs-fs-walk-or-glob-or-fast-glob
|
||||
function walk(directory, ext, filepaths = []) {
|
||||
const files = fs.readdirSync(directory);
|
||||
for (const filename of files) {
|
||||
const filepath = path.join(directory, filename);
|
||||
if (fs.statSync(filepath).isDirectory()) {
|
||||
walk(filepath, ext, filepaths);
|
||||
} else if (path.extname(filename) === ext) {
|
||||
filepaths.push(filepath);
|
||||
}
|
||||
}
|
||||
return filepaths;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/53833620
|
||||
const isSorted = arr => arr.every((v,i,a) => !i || a[i-1] <= v);
|
||||
|
||||
class Validator {
|
||||
constructor(flags) {
|
||||
this.allowDeprecations = flags.includes('-d');
|
||||
this.stopOnError = !flags.includes('-a');
|
||||
this.sortedURLs = flags.includes('-s');
|
||||
this.verbose = flags.includes('-v');
|
||||
|
||||
const schemaPath = path.resolve(__dirname, './scraper.schema.json');
|
||||
this.schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
||||
this.ajv = new Ajv({
|
||||
// allErrors: true,
|
||||
ignoreKeywordsWithRef: true, // should be 'fail' with ajv v7
|
||||
strict: true,
|
||||
});
|
||||
addFormats(this.ajv);
|
||||
|
||||
this.mappingPattern = /^([a-z]+)By(Fragment|Name|URL)$/;
|
||||
}
|
||||
|
||||
run(files) {
|
||||
let scrapers;
|
||||
|
||||
if (files && Array.isArray(files) && files.length > 0) {
|
||||
scrapers = files.map(file => path.resolve(file));
|
||||
} else {
|
||||
const scrapersDir = path.resolve(__dirname, '../scrapers');
|
||||
scrapers = walk(scrapersDir, '.yml');
|
||||
}
|
||||
|
||||
const yamlLoadOptions = {
|
||||
prettyErrors: true,
|
||||
version: '1.2',
|
||||
merge: true,
|
||||
};
|
||||
|
||||
let result = true;
|
||||
const validate = this.ajv.compile(this.schema);
|
||||
|
||||
for (const file of scrapers) {
|
||||
const relPath = path.relative(process.cwd(), file);
|
||||
let contents, data;
|
||||
try {
|
||||
contents = fs.readFileSync(file, 'utf8');
|
||||
data = YAML.parse(contents, yamlLoadOptions);
|
||||
} catch (error) {
|
||||
console.error(`${chalk.red(chalk.bold('ERROR'))} in: ${relPath}:`);
|
||||
error.stack = null;
|
||||
console.error(error);
|
||||
result = result && false;
|
||||
if (this.stopOnError) break;
|
||||
else continue;
|
||||
}
|
||||
|
||||
let valid = validate(data);
|
||||
|
||||
// If schema validation did not pass, don't try to validate mappings.
|
||||
if (valid) {
|
||||
const mappingErrors = this.getMappingErrors(data);
|
||||
const validMapping = mappingErrors.length === 0;
|
||||
if (!validMapping) {
|
||||
validate.errors = (validate.errors || []).concat(mappingErrors);
|
||||
}
|
||||
|
||||
valid = valid && validMapping;
|
||||
}
|
||||
|
||||
// Output validation errors
|
||||
if (!valid) {
|
||||
const output = betterAjvErrors('scraper', data, validate.errors, { indent: 2 });
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
if (this.verbose || !valid) {
|
||||
const validColor = valid ? chalk.green : chalk.red;
|
||||
console.log(`${relPath} Valid: ${validColor(valid)}`);
|
||||
}
|
||||
|
||||
result = result && valid;
|
||||
|
||||
if (!valid && this.stopOnError) break;
|
||||
}
|
||||
|
||||
if (!this.verbose && result) {
|
||||
console.log(chalk.green('Validation passed!'));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getMappingErrors(data) {
|
||||
return [].concat(
|
||||
this._collectConfigMappingErrors(data),
|
||||
this._collectScraperDefinitionErrors(data),
|
||||
this._collectCookieErrors(data),
|
||||
);
|
||||
}
|
||||
|
||||
_collectConfigMappingErrors(data) {
|
||||
const errors = [];
|
||||
|
||||
if (data.sceneByName && !data.sceneByQueryFragment) {
|
||||
errors.push({
|
||||
keyword: 'sceneByName',
|
||||
message: `a \`sceneByQueryFragment\` configuration is required for \`sceneByName\` to work`,
|
||||
params: { keyword: 'sceneByName' },
|
||||
dataPath: '/sceneByName',
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
_collectScraperDefinitionErrors(data) {
|
||||
const hasStashServer = Object.keys(data).includes('stashServer');
|
||||
const xPathScrapers = data.xPathScrapers ? Object.keys(data.xPathScrapers) : [];
|
||||
const jsonScrapers = data.jsonScrapers ? Object.keys(data.jsonScrapers) : [];
|
||||
|
||||
let needsStashServer = false;
|
||||
const configuredXPathScrapers = [];
|
||||
const configuredJsonScrapers = [];
|
||||
|
||||
const errors = [];
|
||||
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const match = this.mappingPattern.exec(key);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seenURLs = {};
|
||||
|
||||
const type = match[1];
|
||||
|
||||
const multiple = value instanceof Array;
|
||||
(multiple ? value : [value]).forEach(({ action, scraper, url }, idx) => {
|
||||
const dataPath = `/${key}${multiple ? `/${idx}` : ''}`;
|
||||
|
||||
if (action === 'stash') {
|
||||
needsStashServer = true;
|
||||
if (!hasStashServer) {
|
||||
errors.push({
|
||||
keyword: 'action',
|
||||
message: `root object should contain a \`stashServer\` definition`,
|
||||
params: { keyword: 'action' },
|
||||
dataPath: dataPath + '/action',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'scrapeXPath') {
|
||||
configuredXPathScrapers.push(scraper);
|
||||
if (!xPathScrapers.includes(scraper)) {
|
||||
errors.push({
|
||||
keyword: 'scraper',
|
||||
message: `xPathScrapers should contain a XPath scraper definition for \`${scraper}\``,
|
||||
params: { keyword: 'scraper' },
|
||||
dataPath: dataPath + '/scraper',
|
||||
});
|
||||
} else if (!data.xPathScrapers || !data.xPathScrapers[scraper][type]) {
|
||||
errors.push({
|
||||
keyword: scraper,
|
||||
message: `\`${scraper}\` should create an object of type \`${type}\``,
|
||||
params: { keyword: scraper },
|
||||
dataPath: `/xPathScrapers/${scraper}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (url) {
|
||||
url.forEach((u, uIdx) => {
|
||||
const exists = seenURLs[u];
|
||||
if (exists) {
|
||||
errors.push({
|
||||
keyword: 'url',
|
||||
message: `URLs for type \`${type}\` should be unique, already exists on ${exists}`,
|
||||
params: { keyword: 'url' },
|
||||
dataPath: `${dataPath}/url/${uIdx}`,
|
||||
});
|
||||
} else {
|
||||
seenURLs[u] = `${dataPath}/url/${uIdx}`;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.sortedURLs && !isSorted(url)) {
|
||||
errors.push({
|
||||
keyword: 'url',
|
||||
message: 'URL list should be sorted in ascending alphabetical order',
|
||||
params: { keyword: 'url' },
|
||||
dataPath: dataPath + '/url',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'scrapeJson') {
|
||||
configuredJsonScrapers.push(scraper);
|
||||
if (!jsonScrapers.includes(scraper)) {
|
||||
errors.push({
|
||||
keyword: 'scraper',
|
||||
message: `jsonScrapers should contain a JSON scraper definition for \`${scraper}\``,
|
||||
params: { keyword: 'scraper' },
|
||||
dataPath: dataPath + '/scraper',
|
||||
});
|
||||
} else if (!data.jsonScrapers || !data.jsonScrapers[scraper][type]) {
|
||||
errors.push({
|
||||
keyword: scraper,
|
||||
message: `\`${scraper}\` should create an object of type \`${type}\``,
|
||||
params: { keyword: scraper },
|
||||
dataPath: `/jsonScrapers/${scraper}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (url) {
|
||||
url.forEach((u, uIdx) => {
|
||||
const exists = seenURLs[u];
|
||||
if (exists) {
|
||||
errors.push({
|
||||
keyword: 'url',
|
||||
message: `URLs for type \`${type}\` should be unique, already exists on ${exists}`,
|
||||
params: { keyword: 'url' },
|
||||
dataPath: `${dataPath}/url/${uIdx}`,
|
||||
});
|
||||
} else {
|
||||
seenURLs[u] = `${dataPath}/url/${uIdx}`;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.sortedURLs && !isSorted(url)) {
|
||||
errors.push({
|
||||
keyword: 'url',
|
||||
message: 'URL list should be sorted in ascending alphabetical order',
|
||||
params: { keyword: 'url' },
|
||||
dataPath: dataPath + '/url',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if (action === 'script') {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// errors.push({
|
||||
// keyword: 'action',
|
||||
// message: `unsupported action \`${action}\``,
|
||||
// params: { keyword: 'action' },
|
||||
// dataPath: dataPath + '/action',
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
// Check for unused definitions
|
||||
|
||||
if (!needsStashServer && hasStashServer) {
|
||||
errors.unshift({
|
||||
keyword: 'stashServer',
|
||||
message: '`stashServer` is defined, but never used',
|
||||
params: { keyword: 'stashServer' },
|
||||
dataPath: '/stashServer',
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
_collectCookieErrors(data) {
|
||||
const errors = [];
|
||||
|
||||
const cookies = data.driver && data.driver.cookies;
|
||||
if (cookies) {
|
||||
const usesCDP = Boolean(data.driver && data.driver.useCDP);
|
||||
cookies.forEach((cookieItem, idx) => {
|
||||
const hasCookieURL = 'CookieURL' in cookieItem;
|
||||
if (!usesCDP && !hasCookieURL) {
|
||||
errors.push({
|
||||
keyword: 'CookieURL',
|
||||
message: '`CookieURL` is required because useCDP is `false`',
|
||||
params: { keyword: 'CookieURL' },
|
||||
dataPath: `/driver/cookies/${idx}`,
|
||||
});
|
||||
} else if (usesCDP && hasCookieURL) {
|
||||
errors.push({
|
||||
keyword: 'CookieURL',
|
||||
message: 'Should not have `CookieURL` because useCDP is `true`',
|
||||
params: { keyword: 'CookieURL' },
|
||||
dataPath: `/driver/cookies/${idx}/CookieURL`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
function main(flags, files) {
|
||||
const args = process.argv.slice(2)
|
||||
flags = (flags === undefined) ? args.filter(arg => arg.startsWith('-')) : flags;
|
||||
files = (files === undefined) ? args.filter(arg => !arg.startsWith('-')) : files;
|
||||
const validator = new Validator(flags);
|
||||
const result = validator.run(files);
|
||||
if (flags.includes('--ci')) {
|
||||
process.exit(result ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = main;
|
||||
module.exports.Validator = Validator;
|
||||
16
validator/package.json
Normal file
16
validator/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "stash-scraper-validator",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"main": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "7",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"better-ajv-errors": "^1.2.0",
|
||||
"chalk": "4",
|
||||
"yaml": "^2.5.1"
|
||||
}
|
||||
}
|
||||
866
validator/scraper.schema.json
Normal file
866
validator/scraper.schema.json
Normal file
@@ -0,0 +1,866 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "scraper",
|
||||
"title": "Stash Scraper",
|
||||
"description": "A stash scraper config",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Scraper Name",
|
||||
"description": "The name of the scraper.",
|
||||
"type": "string"
|
||||
},
|
||||
"sceneByURL": {
|
||||
"title": "Scene by URL",
|
||||
"description": "An array of one or more configurations for scraping scene info by URL(s).",
|
||||
"allOf": [{ "$ref": "#/definitions/anyByURL" }]
|
||||
},
|
||||
"movieByURL": {
|
||||
"title": "Movie by URL",
|
||||
"description": "An array of one or more configurations for scraping movie info by URL(s).",
|
||||
"allOf": [{ "$ref": "#/definitions/anyByURL" }]
|
||||
},
|
||||
"performerByURL": {
|
||||
"title": "Performer by URL",
|
||||
"description": "An array of one or more configurations for scraping performer info by URL(s).",
|
||||
"allOf": [{ "$ref": "#/definitions/anyByURL" }]
|
||||
},
|
||||
"galleryByURL": {
|
||||
"title": "Gallery by URL",
|
||||
"description": "An array of one or more configurations for scraping gallery info by URL(s).",
|
||||
"allOf": [{ "$ref": "#/definitions/anyByURL" }]
|
||||
},
|
||||
"sceneByName": {
|
||||
"title": "Scene by name",
|
||||
"description": "A single configuration for scraping scene info by the scene name.",
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/anyByFragment" },
|
||||
{
|
||||
"propertyNames": {
|
||||
"not": { "enum": ["queryURLReplace"] }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sceneByQueryFragment": {
|
||||
"title": "Scene by query fragment",
|
||||
"description": "A single configuration for scraping scene info by query fragment.",
|
||||
"allOf": [{ "$ref": "#/definitions/anyByFragment" }]
|
||||
},
|
||||
"performerByName": {
|
||||
"title": "Performer by name",
|
||||
"description": "A single configuration for scraping performer info by their name.",
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/anyByFragment" },
|
||||
{
|
||||
"propertyNames": {
|
||||
"not": { "enum": ["queryURLReplace"] }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sceneByFragment": {
|
||||
"title": "Scene by fragment",
|
||||
"description": "A single configuration for scraping scene info by fragment.",
|
||||
"allOf": [{ "$ref": "#/definitions/anyByFragment" }]
|
||||
},
|
||||
"performerByFragment": {
|
||||
"title": "Performer by fragment",
|
||||
"description": "A single configuration for scraping performer info by fragment.",
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/anyByFragment" },
|
||||
{
|
||||
"properties": {
|
||||
"action": { "not": { "enum": ["scrapeXPath", "scrapeJson"] } }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"galleryByFragment": {
|
||||
"title": "Gallery by fragment",
|
||||
"description": "A single configuration for scraping gallery info by fragment.",
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{ "$ref": "#/definitions/anyByFragment" },
|
||||
{
|
||||
"properties": {
|
||||
"action": { "not": { "enum": ["stash"] } }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"xPathScrapers": {
|
||||
"title": "XPath scrapers",
|
||||
"description": "A mapping of scraping configurations using XPath selectors.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^\\w+$": {
|
||||
"title": "XPath scraper",
|
||||
"allOf": [{ "$ref": "#/definitions/scraper" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsonScrapers": {
|
||||
"title": "JSON scrapers",
|
||||
"description": "A mapping of scraping configurations using JSON selectors.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^\\w+$": {
|
||||
"title": "JSON scraper",
|
||||
"allOf": [{ "$ref": "#/definitions/scraper" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"stashServer": {
|
||||
"title": "Stash-Server configuration",
|
||||
"description": "Used to configure a stash server scraping source.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"title": "Debugging",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"printHTML": {
|
||||
"title": "Print HTML",
|
||||
"description": "Print the received HTML/JSON from a scraper request to the log.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"driver": {
|
||||
"title": "Scraper driver options",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"useCDP": {
|
||||
"title": "Use Chrome DevTools Protocol (CDP)",
|
||||
"description": "When set to true, will use the CDP path set in the Stash configuration.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sleep": {
|
||||
"title": "Sleep",
|
||||
"description": "Wait N seconds after the page loads, before performing the scrape.\nIf unset, defaults to 2.",
|
||||
"type": "number",
|
||||
"default": 2,
|
||||
"minimum": 1
|
||||
},
|
||||
"clicks": {
|
||||
"title": "Clicks",
|
||||
"description": "Perform mouse clicks on elements.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["xpath"],
|
||||
"properties": {
|
||||
"xpath": {
|
||||
"title": "Selector",
|
||||
"description": "XPath selector for the element to click.",
|
||||
"type": "string"
|
||||
},
|
||||
"sleep": {
|
||||
"title": "Sleep",
|
||||
"description": "Wait N seconds after after clicking, before continuing the scrape.\nIf unset, defaults to 2.",
|
||||
"type": "number",
|
||||
"default": 2,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Cookie options",
|
||||
"description": "An array of cookie items.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Cookies"],
|
||||
"properties": {
|
||||
"CookieURL": {
|
||||
"title": "URL to apply these cookies to",
|
||||
"description": "Required if `useCDP` is `false`.",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"Cookies": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Domain", "Path", "Name"],
|
||||
"properties": {
|
||||
"Name": {
|
||||
"title": "Cookie name",
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"title": "Cookie value",
|
||||
"type": "string"
|
||||
},
|
||||
"ValueRandom": {
|
||||
"title": "Generate a random string with the length of N (> 0) characters",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"Domain": {
|
||||
"title": "Cookie domain",
|
||||
"type": "string"
|
||||
},
|
||||
"Path": {
|
||||
"title": "Cookie path",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["ValueRandom"] },
|
||||
{ "required": ["Value"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"title": "Headers",
|
||||
"description": "Headers to add to scraper requests.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Key", "Value"],
|
||||
"properties": {
|
||||
"Key": {
|
||||
"title": "Key",
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"title": "Value",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"anyByURL": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["action", "url"],
|
||||
"properties": {
|
||||
"action": {
|
||||
"title": "Action",
|
||||
"description": "Scrape method to use",
|
||||
"enum": ["script", "scrapeXPath", "scrapeJson"]
|
||||
},
|
||||
"url": {
|
||||
"title": "URLs",
|
||||
"description": "An array of partial URLs that this configuration applies to.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"title": "URL",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"scraper": {
|
||||
"title": "Scraper definition name",
|
||||
"description": "The name of the scraper definition, as defined in xPathScrapers or jsonScrapers, according to the defined action.",
|
||||
"type": "string"
|
||||
},
|
||||
"script": {
|
||||
"title": "Script",
|
||||
"description": "A list of an executable and arguments to be used as a scraper.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"title": "Script executable/argument",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"queryURL": {
|
||||
"title": "Query URL",
|
||||
"type": "string"
|
||||
},
|
||||
"queryURLReplace": {
|
||||
"title": "Query URL Replace",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"propertyNames": {
|
||||
"enum": ["url"]
|
||||
},
|
||||
"patternProperties": {
|
||||
"^\\w+$": {
|
||||
"$ref": "#/definitions/replaceKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": { "enum": ["scrapeXPath", "scrapeJson"] }
|
||||
}
|
||||
},
|
||||
"then": { "required": ["scraper"] }
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": { "const": "script" }
|
||||
}
|
||||
},
|
||||
"then": { "required": ["script"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"anyByFragment": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["action"],
|
||||
"properties": {
|
||||
"action": {
|
||||
"title": "Action",
|
||||
"description": "Scrape method to use",
|
||||
"enum": ["script", "scrapeXPath", "scrapeJson", "stash"]
|
||||
},
|
||||
"scraper": {
|
||||
"title": "Scraper definition name",
|
||||
"description": "The name of the scraper definition, as defined in xPathScrapers or jsonScrapers, according to the defined action.",
|
||||
"type": "string"
|
||||
},
|
||||
"script": {
|
||||
"title": "Script",
|
||||
"description": "A list of an executable and arguments to be used as a scraper.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"title": "Script executable/argument",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"queryURL": {
|
||||
"title": "Query URL",
|
||||
"type": "string"
|
||||
},
|
||||
"queryURLReplace": {
|
||||
"title": "Query URL Replace",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"propertyNames": {
|
||||
"enum": ["checksum", "filename", "oshash", "title", "url"]
|
||||
},
|
||||
"patternProperties": {
|
||||
"^\\w+$": {
|
||||
"$ref": "#/definitions/replaceKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": { "enum": ["scrapeXPath", "scrapeJson"] }
|
||||
}
|
||||
},
|
||||
"then": { "required": ["scraper", "queryURL"] }
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": { "const": "script" }
|
||||
}
|
||||
},
|
||||
"then": { "required": ["script"] }
|
||||
}
|
||||
]
|
||||
},
|
||||
"scraper": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"common": {
|
||||
"title": "Common selectors",
|
||||
"description": "Common selector fragments that can be referenced in the selector strings.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^[$].+$": {
|
||||
"title": "Selector",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scene": { "$ref": "#/definitions/sceneObject" },
|
||||
"performer": { "$ref": "#/definitions/performerObject" },
|
||||
"movie": { "$ref": "#/definitions/movieObject" },
|
||||
"gallery": { "$ref": "#/definitions/galleryObject" }
|
||||
}
|
||||
},
|
||||
"sceneObject": {
|
||||
"title": "Scene object",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"Title": {
|
||||
"title": "Scene title",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Details": {
|
||||
"title": "Scene details",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"URL": {
|
||||
"title": "Scene URL",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Date": {
|
||||
"title": "Scene date",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Image": {
|
||||
"title": "Scene image",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Studio": {
|
||||
"title": "Scene studio",
|
||||
"allOf": [{ "$ref": "#/definitions/studioObject" }]
|
||||
},
|
||||
"Movies": {
|
||||
"title": "Scene movies",
|
||||
"allOf": [{ "$ref": "#/definitions/movieObject" }]
|
||||
},
|
||||
"Tags": {
|
||||
"title": "Scene tags",
|
||||
"allOf": [{ "$ref": "#/definitions/tagsObject" }]
|
||||
},
|
||||
"Performers": {
|
||||
"title": "Scene performers",
|
||||
"allOf": [{ "$ref": "#/definitions/performerObject" }]
|
||||
},
|
||||
"Code": {
|
||||
"title": "Scene studio code",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Director": {
|
||||
"title": "Scene director",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"performerObject": {
|
||||
"title": "Performer object",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Name"],
|
||||
"properties": {
|
||||
"Name": {
|
||||
"title": "Performer name",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Disambiguation": {
|
||||
"title": "Performer disambiguation",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Aliases": {
|
||||
"title": "Performer aliases",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Gender": {
|
||||
"title": "Performer gender",
|
||||
"description": "Must be one of (case insensitive): `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary`",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"URL": {
|
||||
"title": "Performer URL",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Twitter": {
|
||||
"title": "Performer Twitter",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Instagram": {
|
||||
"title": "Performer Instagram",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Birthdate": {
|
||||
"title": "Performer birthdate",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"DeathDate": {
|
||||
"title": "Performer death date",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Ethnicity": {
|
||||
"title": "Performer ethnicity",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Country": {
|
||||
"title": "Performer country",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"HairColor": {
|
||||
"title": "Performer hair color",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"EyeColor": {
|
||||
"title": "Performer eye color",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Height": {
|
||||
"title": "Performer height",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Weight": {
|
||||
"title": "Performer weight",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Measurements": {
|
||||
"title": "Performer measurements",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"FakeTits": {
|
||||
"title": "Performer fake tits",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"PenisLength": {
|
||||
"title": "Performer penis length",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Circumcised": {
|
||||
"title": "Performer circumcised",
|
||||
"description": "Must be one of (case insensitive): `cut`, `uncut`",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"CareerLength": {
|
||||
"title": "Performer career length",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Tattoos": {
|
||||
"title": "Performer tattoos",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Piercings": {
|
||||
"title": "Performer piercings",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Details": {
|
||||
"title": "Performer details",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Tags": {
|
||||
"title": "Performer tags",
|
||||
"allOf": [{ "$ref": "#/definitions/tagsObject" }]
|
||||
},
|
||||
"Image": {
|
||||
"title": "Performer image",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"movieObject": {
|
||||
"title": "Movie object",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Name"],
|
||||
"properties": {
|
||||
"Name": {
|
||||
"title": "Movie name",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Date": {
|
||||
"title": "Movie date",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Duration": {
|
||||
"title": "Movie duration",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Director": {
|
||||
"title": "Movie director",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Synopsis": {
|
||||
"title": "Movie synopsis",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Studio": {
|
||||
"title": "Movie studio",
|
||||
"allOf": [{ "$ref": "#/definitions/studioObject" }]
|
||||
},
|
||||
"FrontImage": {
|
||||
"title": "Movie front image",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"BackImage": {
|
||||
"title": "Movie back image",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"URL": {
|
||||
"title": "Movie URL",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Aliases": {
|
||||
"title": "Movie aliases",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Tags": {
|
||||
"title": "Movie tags",
|
||||
"allOf": [{ "$ref": "#/definitions/tagsObject" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"galleryObject": {
|
||||
"title": "Gallery object",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Title"],
|
||||
"properties": {
|
||||
"Title": {
|
||||
"title": "Gallery title",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Code": {
|
||||
"title": "Gallery studio code",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Details": {
|
||||
"title": "Gallery details",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"URL": {
|
||||
"title": "Gallery URL",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Date": {
|
||||
"title": "Gallery date",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"Studio": {
|
||||
"title": "Gallery studio",
|
||||
"allOf": [{ "$ref": "#/definitions/studioObject" }]
|
||||
},
|
||||
"Tags": {
|
||||
"title": "Gallery tags",
|
||||
"allOf": [{ "$ref": "#/definitions/tagsObject" }]
|
||||
},
|
||||
"Performers": {
|
||||
"title": "Gallery performers",
|
||||
"allOf": [{ "$ref": "#/definitions/performerObject" }]
|
||||
},
|
||||
"Photographer": {
|
||||
"title": "Gallery photographer",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"studioObject": {
|
||||
"title": "Studio object",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Name"],
|
||||
"properties": {
|
||||
"Name": {
|
||||
"title": "Studio name",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"URL": {
|
||||
"title": "Studio URL",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tagsObject": {
|
||||
"title": "Tags object",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["Name"],
|
||||
"properties": {
|
||||
"Name": {
|
||||
"title": "Tag name",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"replaceKey": {
|
||||
"title": "Regular Expression replacements",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["regex", "with"],
|
||||
"properties": {
|
||||
"regex": {
|
||||
"title": "Pattern to search for",
|
||||
"type": "string"
|
||||
},
|
||||
"with": {
|
||||
"title": "Replace matched pattern with",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nodeSelector": {
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Simple selector",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
{
|
||||
"title": "Fixed value",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["fixed"],
|
||||
"properties": {
|
||||
"fixed": {
|
||||
"title": "Fixed value",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Complex selector",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["selector"],
|
||||
"properties": {
|
||||
"selector": {
|
||||
"title": "Selector",
|
||||
"type": "string"
|
||||
},
|
||||
"postProcess": {
|
||||
"title": "Post-processing operations",
|
||||
"description": "Alter the value with one or more operations.\nPost-processing operations are performed in the order they are specified.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"title": "Post-processing operation",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1,
|
||||
"properties": {
|
||||
"parseDate": {
|
||||
"title": "Parse date from value",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"January 2, 2006",
|
||||
"Jan 02, 2006",
|
||||
"2006-01-02",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05-07:00"
|
||||
]
|
||||
},
|
||||
"replace": {
|
||||
"$ref": "#/definitions/replaceKey"
|
||||
},
|
||||
"map": {
|
||||
"title": "Map input values to output values",
|
||||
"description": "If the value matches one of the input values, it is replaced with the matching output value. If no value is matched, then value is unmodified.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^.+$": {
|
||||
"title": "Replace matched string with value",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subScraper": {
|
||||
"title": "Sub-scraper",
|
||||
"description": "Makes an HTTP request using the current value as the URL and applies a scraping configuration to the response.",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"feetToCm": {
|
||||
"title": "Feet to Centimetres",
|
||||
"description": "Convert height measurement in feet and inches to centimetres.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"dimensionToMetric": {
|
||||
"title": "Convert dimension to metric",
|
||||
"description": "Smart conversion to centimeters: can handle feet, inches or centimeters.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"lbToKg": {
|
||||
"title": "Pounds to Kilograms",
|
||||
"description": "Convert weight measurement in pounds to kilograms.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"subtractDays": {
|
||||
"title": "Subtract Days",
|
||||
"description": "Subtracts the value in days from the current date.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"javascript": {
|
||||
"title": "Execute JavaScript",
|
||||
"description": "Executes the given JavaScript code and uses the result as the value.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"concat": {
|
||||
"title": "Concatenate",
|
||||
"description": "If a selector matches multiple elements, all of the elements will be joined using the separator given.\nExecuted before `postProcess`.",
|
||||
"type": "string"
|
||||
},
|
||||
"replace": {
|
||||
"deprecated": true,
|
||||
"title": "[DEPRECATED] Regular Expression replacements",
|
||||
"description": "[DEPRECATED] Use `postProcess[].replace`",
|
||||
"allOf": [{ "$ref": "#/definitions/replaceKey" }]
|
||||
},
|
||||
"subScraper": {
|
||||
"deprecated": true,
|
||||
"title": "[DEPRECATED] Sub-scraper",
|
||||
"description": "[DEPRECATED] Use `postProcess[].subScraper`",
|
||||
"allOf": [{ "$ref": "#/definitions/nodeSelector" }]
|
||||
},
|
||||
"parseDate": {
|
||||
"deprecated": true,
|
||||
"title": "[DEPRECATED] Parse date from value",
|
||||
"description": "[DEPRECATED] Use `postProcess[].parseDate`",
|
||||
"type": "string"
|
||||
},
|
||||
"split": {
|
||||
"title": "Split",
|
||||
"description": "Splits a string to more elements using the separator given.\nExecuted after `postProcess`.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
210
validator/yarn.lock
Normal file
210
validator/yarn.lock
Normal file
@@ -0,0 +1,210 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/code-frame@^7.16.0":
|
||||
version "7.24.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
|
||||
integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.24.7"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/helper-validator-identifier@^7.24.7":
|
||||
version "7.24.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
|
||||
integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
|
||||
|
||||
"@babel/highlight@^7.24.7":
|
||||
version "7.24.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d"
|
||||
integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.24.7"
|
||||
chalk "^2.4.2"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@humanwhocodes/momoa@^2.0.2":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/momoa/-/momoa-2.0.4.tgz#8b9e7a629651d15009c3587d07a222deeb829385"
|
||||
integrity sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==
|
||||
|
||||
ajv-formats@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578"
|
||||
integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==
|
||||
dependencies:
|
||||
ajv "^8.0.0"
|
||||
|
||||
ajv@7:
|
||||
version "7.2.4"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.4.tgz#8e239d4d56cf884bccca8cca362f508446dc160f"
|
||||
integrity sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^8.0.0:
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
|
||||
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
fast-uri "^3.0.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
better-ajv-errors@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/better-ajv-errors/-/better-ajv-errors-1.2.0.tgz#6412d58fa4d460ff6ccbd9e65c5fef9781cc5286"
|
||||
integrity sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.16.0"
|
||||
"@humanwhocodes/momoa" "^2.0.2"
|
||||
chalk "^4.1.2"
|
||||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0 < 4"
|
||||
|
||||
chalk@4, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
|
||||
dependencies:
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
dependencies:
|
||||
color-name "1.1.3"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-uri@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134"
|
||||
integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
|
||||
|
||||
has-flag@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
json-schema-traverse@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
jsonpointer@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
|
||||
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
|
||||
|
||||
"leven@^3.1.0 < 4":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||
integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
|
||||
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
|
||||
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
yaml@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130"
|
||||
integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==
|
||||
Reference in New Issue
Block a user