This commit is contained in:
DogmaDragon
2024-12-31 12:06:03 +02:00
parent 386b7ceee4
commit 4a1db96626
11 changed files with 1644 additions and 1 deletions

5
.gitattributes vendored Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
{
"yaml.schemas": {
"validator/scraper.schema.json": "*.yml"
},
"python.analysis.typeCheckingMode": "basic"
}

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
'use strict';
require('./validator/index.js')();

357
validator/index.js Normal file
View 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
View 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"
}
}

View 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
View 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==