🍼 Cleaned up the OPDS feed
This commit is contained in:
@@ -71,6 +71,7 @@
|
|||||||
"websocket": "^1.0.34",
|
"websocket": "^1.0.34",
|
||||||
"ws": "^7.5.3",
|
"ws": "^7.5.3",
|
||||||
"ws-calibre": "https://github.com/bluelovers/ws-calibre",
|
"ws-calibre": "https://github.com/bluelovers/ws-calibre",
|
||||||
|
"xml2js": "^0.4.23",
|
||||||
"xregexp": "^5.0.2"
|
"xregexp": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export const search = (data: SearchData) => async (dispatch) => {
|
|||||||
await SocketService.connect("admin", "password", true);
|
await SocketService.connect("admin", "password", true);
|
||||||
}
|
}
|
||||||
const instance: SearchInstance = await SocketService.post("search");
|
const instance: SearchInstance = await SocketService.post("search");
|
||||||
console.log(instance);
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: AIRDCPP_SEARCH_IN_PROGRESS,
|
type: AIRDCPP_SEARCH_IN_PROGRESS,
|
||||||
});
|
});
|
||||||
@@ -98,7 +97,7 @@ export const search = (data: SearchData) => async (dispatch) => {
|
|||||||
// Finally, perform the actual search
|
// Finally, perform the actual search
|
||||||
await SocketService.post(`search/${instance.id}/hub_search`, data);
|
await SocketService.post(`search/${instance.id}/hub_search`, data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("ERO", error);
|
console.log(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -185,7 +184,6 @@ export const getBundlesForComic =
|
|||||||
if (!SocketService.isConnected()) {
|
if (!SocketService.isConnected()) {
|
||||||
await SocketService.connect("admin", "password", true);
|
await SocketService.connect("admin", "password", true);
|
||||||
}
|
}
|
||||||
// const bundles = await SocketService.get("queue/bundles/0/50");
|
|
||||||
const comicObject = await axios({
|
const comicObject = await axios({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "http://localhost:3000/api/import/getComicBookById",
|
url: "http://localhost:3000/api/import/getComicBookById",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ $border-color: red;
|
|||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
.recent-comics-column {
|
.recent-comics-column {
|
||||||
padding-left: 25px; /* gutter size */
|
padding-left: 22px; /* gutter size */
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
& > div {
|
& > div {
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
/* change div to reference your elements you put in <Masonry> */
|
||||||
@@ -82,7 +82,7 @@ $border-color: red;
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.volumes-grid-column {
|
.volumes-grid-column {
|
||||||
padding-left: 25px; /* gutter size */
|
padding-left: 22px; /* gutter size */
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
& > div {
|
& > div {
|
||||||
/* change div to reference your elements you put in <Masonry> */
|
/* change div to reference your elements you put in <Masonry> */
|
||||||
@@ -244,6 +244,11 @@ $border-color: red;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AirDC++ search results
|
||||||
|
.dupe-search-result {
|
||||||
|
background: lavender;
|
||||||
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
.search {
|
.search {
|
||||||
.main-search-bar {
|
.main-search-bar {
|
||||||
@@ -363,6 +368,7 @@ $border-color: red;
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
// progress
|
// progress
|
||||||
.progress-indicator-container {
|
.progress-indicator-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const AcquisitionPanel = (
|
|||||||
dispatch(
|
dispatch(
|
||||||
downloadAirDCPPItem(searchInstanceId, resultId, comicBookObjectId),
|
downloadAirDCPPItem(searchInstanceId, resultId, comicBookObjectId),
|
||||||
);
|
);
|
||||||
|
// this is to update the download count badge on the downloads tab
|
||||||
dispatch(getBundlesForComic(comicBookObjectId));
|
dispatch(getBundlesForComic(comicBookObjectId));
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
@@ -128,7 +129,10 @@ export const AcquisitionPanel = (
|
|||||||
<tbody>
|
<tbody>
|
||||||
{map(airDCPPSearchResults, ({ result }, idx) => {
|
{map(airDCPPSearchResults, ({ result }, idx) => {
|
||||||
return (
|
return (
|
||||||
<tr key={idx}>
|
<tr
|
||||||
|
key={idx}
|
||||||
|
className={!isNil(result.dupe) ? "dupe-search-result" : ""}
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
{result.type.id === "directory" ? (
|
{result.type.id === "directory" ? (
|
||||||
@@ -136,9 +140,13 @@ export const AcquisitionPanel = (
|
|||||||
) : null}{" "}
|
) : null}{" "}
|
||||||
{ellipsize(result.name, 70)}
|
{ellipsize(result.name, 70)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
<dd>
|
<dd>
|
||||||
<div className="tags">
|
<div className="tags">
|
||||||
|
{!isNil(result.dupe) ? (
|
||||||
|
<span className="tag is-warning">Dupe</span>
|
||||||
|
) : null}
|
||||||
<span className="tag is-light is-info">
|
<span className="tag is-light is-info">
|
||||||
{result.users.user.nicks}
|
{result.users.user.nicks}
|
||||||
</span>
|
</span>
|
||||||
@@ -169,17 +177,17 @@ export const AcquisitionPanel = (
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadDCPPResult(
|
downloadDCPPResult(
|
||||||
searchInstance.id,
|
searchInstance.id,
|
||||||
result.id,
|
result.id,
|
||||||
props.comicBookMetadata._id,
|
props.comicBookMetadata._id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="fas fa-file-download"></i>
|
<i className="fas fa-file-download"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { useEffect, ReactElement } from "react";
|
import React, { useEffect, ReactElement } from "react";
|
||||||
import { getDownloadProgress } from "../actions/airdcpp.actions";
|
import {
|
||||||
|
getDownloadProgress,
|
||||||
|
getBundlesForComic,
|
||||||
|
} from "../actions/airdcpp.actions";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { RootState } from "threetwo-ui-typings";
|
import { RootState } from "threetwo-ui-typings";
|
||||||
import { isNil, map } from "lodash";
|
import { isNil, map } from "lodash";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import ellipsize from "ellipsize";
|
||||||
|
|
||||||
interface IDownloadsPanelProps {
|
interface IDownloadsPanelProps {
|
||||||
data: any;
|
data: any;
|
||||||
@@ -16,56 +20,87 @@ export const DownloadsPanel = (
|
|||||||
const downloadProgressTick = useSelector(
|
const downloadProgressTick = useSelector(
|
||||||
(state: RootState) => state.airdcpp.downloadProgressData,
|
(state: RootState) => state.airdcpp.downloadProgressData,
|
||||||
);
|
);
|
||||||
|
const bundles = useSelector((state: RootState) => {
|
||||||
|
return state.airdcpp.bundles;
|
||||||
|
});
|
||||||
|
console.log("BANDYA", bundles);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// dispatch(getDownloadProgress(props.data._id));
|
dispatch(getBundlesForComic(props.comicObjectId));
|
||||||
// }, [dispatch]);
|
dispatch(getDownloadProgress(props.comicObjectId));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const ProgressTick = (props) => (
|
const ProgressTick = (props) => {
|
||||||
<div className="column is-one-quarter">
|
console.log("tick", props);
|
||||||
{JSON.stringify(props.downloadProgressTick)}
|
|
||||||
<progress
|
|
||||||
className="progress is-small is-success"
|
|
||||||
value={props.downloaded_bytes}
|
|
||||||
max={props.size}
|
|
||||||
>
|
|
||||||
{(parseInt(props.downloaded_bytes) / parseInt(props.size)) * 100}%
|
|
||||||
</progress>
|
|
||||||
<dl>
|
|
||||||
<dt>{props.name}</dt>
|
|
||||||
<dd>
|
|
||||||
{prettyBytes(props.downloaded_bytes)} of
|
|
||||||
{prettyBytes(props.size)}
|
|
||||||
</dd>
|
|
||||||
<dd>
|
|
||||||
{prettyBytes(props.speed)} per second. Time left:
|
|
||||||
{parseInt(props.seconds_left) / 60}
|
|
||||||
</dd>
|
|
||||||
<dd>{props.target}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Bundles = (props) => {
|
|
||||||
console.log(props)
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="column is-half">
|
||||||
<dl>
|
{JSON.stringify(props.data.downloadProgressTick)}
|
||||||
{!isNil(props.data) &&
|
<progress
|
||||||
props.data &&
|
className="progress is-small is-success"
|
||||||
map(props.data, (bundle) => (
|
value={props.data.downloaded_bytes}
|
||||||
<span key={bundle.id}>
|
max={props.data.size}
|
||||||
<dt>{bundle.name}</dt>
|
>
|
||||||
<dd>{bundle.target}</dd>
|
{(parseInt(props.data.downloaded_bytes) / parseInt(props.data.size)) *
|
||||||
<dd>{bundle.size}</dd>
|
100}
|
||||||
</span>
|
%
|
||||||
))}
|
</progress>
|
||||||
</dl>
|
<div className="card">
|
||||||
|
<div className="card-content is-size-7">
|
||||||
|
<dl>
|
||||||
|
<dt>{props.data.name}</dt>
|
||||||
|
<dd>
|
||||||
|
{prettyBytes(props.data.downloaded_bytes)} of{" "}
|
||||||
|
{prettyBytes(props.data.size)}
|
||||||
|
</dd>
|
||||||
|
<dd>{prettyBytes(props.data.speed)} per second.</dd>
|
||||||
|
<dd>
|
||||||
|
Time left:
|
||||||
|
{parseInt(props.data.seconds_left) / 60}
|
||||||
|
</dd>
|
||||||
|
<dd>{props.data.target}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return !isNil(props.data) ? <Bundles data={props.data} /> : null;
|
const Bundles = (props) => {
|
||||||
|
console.log(props);
|
||||||
|
return (
|
||||||
|
<table className="table is-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Filename</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{!isNil(props.data) &&
|
||||||
|
props.data &&
|
||||||
|
map(props.data, (bundle) => (
|
||||||
|
<tr key={bundle.id}>
|
||||||
|
<td>
|
||||||
|
<h5>{ellipsize(bundle.name, 58)}</h5>
|
||||||
|
<span className="is-size-7">{bundle.target}</span>
|
||||||
|
</td>
|
||||||
|
<td>{prettyBytes(bundle.size)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return !isNil(props.data) ? (
|
||||||
|
<>
|
||||||
|
{!isNil(downloadProgressTick) ? (
|
||||||
|
<ProgressTick data={downloadProgressTick} />
|
||||||
|
) : null}
|
||||||
|
<Bundles data={bundles} />
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DownloadsPanel;
|
export default DownloadsPanel;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const VolumeGroups = (): ReactElement => {
|
|||||||
map(volumeGroups.data, (group) => {
|
map(volumeGroups.data, (group) => {
|
||||||
if (!isNil(group)) {
|
if (!isNil(group)) {
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack" key={group.results.id}>
|
||||||
<img src={group.results.image.small_url} />
|
<img src={group.results.image.small_url} />
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="stack-title">
|
<div className="stack-title">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import io, { Socket } from "socket.io-client";
|
|||||||
import { SOCKET_BASE_URI } from "../../constants/endpoints";
|
import { SOCKET_BASE_URI } from "../../constants/endpoints";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { RMQ_SOCKET_CONNECTED } from "../../constants/action-types";
|
import { RMQ_SOCKET_CONNECTED } from "../../constants/action-types";
|
||||||
|
import { isNil } from "lodash";
|
||||||
|
|
||||||
const WebSocketContext = createContext(null);
|
const WebSocketContext = createContext(null);
|
||||||
export const WebSocketProvider = ({ children }) => {
|
export const WebSocketProvider = ({ children }) => {
|
||||||
@@ -10,7 +11,7 @@ export const WebSocketProvider = ({ children }) => {
|
|||||||
let ws;
|
let ws;
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
if (!socket) {
|
if (!isNil(socket)) {
|
||||||
socket = io(SOCKET_BASE_URI);
|
socket = io(SOCKET_BASE_URI);
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { async as FastGlob } from "@bluelovers/fast-glob/bluebird";
|
|||||||
import { Entry, Feed } from "opds-extra/lib/v1";
|
import { Entry, Feed } from "opds-extra/lib/v1";
|
||||||
import { Link } from "opds-extra/lib/v1/core";
|
import { Link } from "opds-extra/lib/v1/core";
|
||||||
import router from "../router";
|
import router from "../router";
|
||||||
|
import xml2js from "xml2js";
|
||||||
|
|
||||||
const path_of_books = "/Users/rishi/work/threetwo/src/server/comics";
|
const path_of_books = "/Users/rishi/work/threetwo/src/server/comics";
|
||||||
router.use("/opds", async (req, res, next) => {
|
router.use("/opds", async (req, res, next) => {
|
||||||
@@ -18,22 +19,17 @@ router.use("/opds", async (req, res, next) => {
|
|||||||
title: `title`,
|
title: `title`,
|
||||||
subtitle: `subtitle`,
|
subtitle: `subtitle`,
|
||||||
icon: "/favicon.ico",
|
icon: "/favicon.ico",
|
||||||
link: {
|
|
||||||
rel: EnumLinkRel.SELF,
|
|
||||||
href: "/opds-catalogs/root.xml",
|
|
||||||
type: "application/atom+xml;profile=opds-catalog;kind=navigation",
|
|
||||||
} as Link,
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
async (feed: Feed) => {
|
async (feed: Feed) => {
|
||||||
feed.id = "bastard12312899lfh-1238989123-1231289812";
|
feed.id = "urn:uuid:2853dacf-ed79-42f5-8e8a-a7bb3d1ae6a2";
|
||||||
feed.books = feed.books || [];
|
feed.books = feed.books || [];
|
||||||
await FastGlob(["*.cbr", "*.cbz", "*.cb7", "*.cba", "*.cbt"], {
|
await FastGlob(["*.cbr", "*.cbz", "*.cb7", "*.cba", "*.cbt"], {
|
||||||
cwd: path_of_books,
|
cwd: path_of_books,
|
||||||
}).each((file, idx) => {
|
}).each((file, idx) => {
|
||||||
const ext = extname(file);
|
const ext = extname(file);
|
||||||
const title = basename(file, ext);
|
const title = basename(file, ext);
|
||||||
const href = encodeURI(`/file/${file}`);
|
const href = encodeURI(`/api/file/${file}`);
|
||||||
const type = lookup(ext) || "application/octet-stream";
|
const type = lookup(ext) || "application/octet-stream";
|
||||||
|
|
||||||
const entry = Entry.deserialize<Entry>({
|
const entry = Entry.deserialize<Entry>({
|
||||||
@@ -49,7 +45,6 @@ router.use("/opds", async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isUndefined(feed) && !isUndefined(feed.books)) {
|
if (!isUndefined(feed) && !isUndefined(feed.books)) {
|
||||||
console.log("ENTRY", entry)
|
|
||||||
feed.books.push(entry);
|
feed.books.push(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -59,8 +54,35 @@ router.use("/opds", async (req, res, next) => {
|
|||||||
],
|
],
|
||||||
).then((feed) => {
|
).then((feed) => {
|
||||||
res.setHeader("Content-Type", "application/xml");
|
res.setHeader("Content-Type", "application/xml");
|
||||||
console.log("FFFEEDD", feed);
|
let data;
|
||||||
return res.end(feed.toXML());
|
xml2js.parseString(feed.toXML(), (err, result) => {
|
||||||
|
result.feed.link = {
|
||||||
|
$: {
|
||||||
|
rel: "self",
|
||||||
|
href: "/opds-catalogs/root.xml",
|
||||||
|
type: "application/atom+xml;profile=opds-catalog;kind=navigation",
|
||||||
|
},
|
||||||
|
_: "",
|
||||||
|
};
|
||||||
|
const builder = new xml2js.Builder({
|
||||||
|
xmldec: {
|
||||||
|
version: "1.0",
|
||||||
|
encoding: "UTF-8",
|
||||||
|
standalone: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
data = builder.buildObject(result, {
|
||||||
|
renderOpts: {
|
||||||
|
pretty: true,
|
||||||
|
indent: " ",
|
||||||
|
newline: "\n",
|
||||||
|
allowEmpty: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// write data to file
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
return res.end(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
15
yarn.lock
15
yarn.lock
@@ -11749,7 +11749,7 @@ sass-loader@^11.0.1:
|
|||||||
klona "^2.0.4"
|
klona "^2.0.4"
|
||||||
neo-async "^2.6.2"
|
neo-async "^2.6.2"
|
||||||
|
|
||||||
sax@~1.2.4:
|
sax@>=0.6.0, sax@~1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz"
|
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
@@ -14141,11 +14141,24 @@ xml-schema2@^3.0.1:
|
|||||||
xml-parser "1.2.1"
|
xml-parser "1.2.1"
|
||||||
xmlbuilder "15.1.1"
|
xmlbuilder "15.1.1"
|
||||||
|
|
||||||
|
xml2js@^0.4.23:
|
||||||
|
version "0.4.23"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
|
||||||
|
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
|
||||||
|
dependencies:
|
||||||
|
sax ">=0.6.0"
|
||||||
|
xmlbuilder "~11.0.0"
|
||||||
|
|
||||||
xmlbuilder@15.1.1:
|
xmlbuilder@15.1.1:
|
||||||
version "15.1.1"
|
version "15.1.1"
|
||||||
resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz"
|
resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz"
|
||||||
integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
|
integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
|
||||||
|
|
||||||
|
xmlbuilder@~11.0.0:
|
||||||
|
version "11.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
||||||
|
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
|
||||||
|
|
||||||
xmlchars@^2.2.0:
|
xmlchars@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user