version/0.8 #7
10
README.md
10
README.md
|
@ -6,9 +6,9 @@ Un motore di ricerca libri basato su protocollo IPFS.
|
||||||
|
|
||||||
Una versione funzionante si trova [qui](https://millelibri.copyriot.xyz).
|
Una versione funzionante si trova [qui](https://millelibri.copyriot.xyz).
|
||||||
|
|
||||||
Il progetto è partito come un fork di [Book Searcher](https://github.com/book-searcher-org/book-searcher).
|
Un tempo (fino a dicembre 2023) il progetto era sincronizzato con [Book Searcher](https://github.com/book-searcher-org/book-searcher). Poi venne la macchina editoriale e distrusse tutto:
|
||||||
|
|
||||||
L'obiettivo è trasformarlo in una piattaforma partecipativa: gli utenti avranno la possibilità di richiedere l'aggiunta di libri, oltre a fare ricerche.
|
https://github.com/book-searcher-org/deleted/issues/1
|
||||||
|
|
||||||
## Installazione rapida
|
## Installazione rapida
|
||||||
|
|
||||||
|
@ -32,8 +32,4 @@ Per modificare la parte frontend (React) del progetto, posizionarsi nella cartel
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Licenza
|
|
||||||
|
|
||||||
Book Searcher è rilasciato sotto licenza [BSD-3-Clause](https://github.com/book-searcher-org/book-searcher/blob/master/LICENSE). Millelibri eredita la licenza da Book Searcher.
|
|
|
@ -4,18 +4,10 @@ services:
|
||||||
zlib:
|
zlib:
|
||||||
image: lamacchinadesiderante/millelibri:latest
|
image: lamacchinadesiderante/millelibri:latest
|
||||||
|
|
||||||
# image: millelibri:v0.4
|
|
||||||
|
|
||||||
# image: millelibri
|
|
||||||
|
|
||||||
# build:
|
|
||||||
# context: .
|
|
||||||
# dockerfile: ./Dockerfile
|
|
||||||
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "7070:7070"
|
- "7070:7070"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./index:/index
|
- ./index:/index
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
# .env.production
|
# .env.production
|
||||||
VITE_BACKEND_BASE_API = 'http://127.0.0.1:7070/'
|
VITE_BACKEND_BASE_API = 'http://127.0.0.1:7070/'
|
||||||
VITE_STORJ_JSON_URL = 'https://link.storjshare.io/juowot6xaa2rz4vjqeal2uo44jka/millelibri%2Findex%2Fjson%2Fmillelibri.json?download=1'
|
|
|
@ -1,3 +1,2 @@
|
||||||
# .env.production
|
# .env.production
|
||||||
VITE_BACKEND_BASE_API = ''
|
VITE_BACKEND_BASE_API = ''
|
||||||
VITE_STORJ_JSON_URL = 'https://link.storjshare.io/juowot6xaa2rz4vjqeal2uo44jka/millelibri%2Findex%2Fjson%2Fmillelibri.json?download=1'
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "v0.7.0",
|
"version": "v0.8.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": "https://git.lamacchinadesiderante.org/lamacchinadesiderante/millelibri",
|
"repository": "https://git.lamacchinadesiderante.org/lamacchinadesiderante/millelibri",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
"@chakra-ui/react": "^2.4.6",
|
"@chakra-ui/react": "^2.4.6",
|
||||||
"@chakra-ui/skip-nav": "^2.0.13",
|
"@chakra-ui/skip-nav": "^2.0.13",
|
||||||
"@chakra-ui/system": "^2.3.7",
|
"@chakra-ui/system": "^2.3.7",
|
||||||
|
"@chakra-ui/icons": "^2.1.1",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@tanstack/react-table": "^8.7.4",
|
"@tanstack/react-table": "^8.7.4",
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
"framer-motion": "^7.10.3",
|
"framer-motion": "^7.10.3",
|
||||||
"i18next": "^22.4.6",
|
"i18next": "^22.4.6",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
|
"js-file-download": "^0.4.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
||||||
import { Card, CardHeader, Heading, Divider, CardBody, CardFooter, GridItem, SimpleGrid, Text, Button, Flex, Icon } from '@chakra-ui/react';
|
import { Box, Card, CardHeader, Heading, Divider, CardBody, CardFooter, GridItem, SimpleGrid, Text, Button, Flex, Icon } from '@chakra-ui/react';
|
||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import { filesize as formatFileSize } from 'filesize';
|
||||||
import { TbChevronUp } from 'react-icons/tb';
|
import { TbChevronUp } from 'react-icons/tb';
|
||||||
import ExternalLink from './ExternalLink';
|
import ExternalLink from './ExternalLink';
|
||||||
import Description from './Description';
|
import Description from './Description';
|
||||||
|
import IpfsDownloadButton from './IpfsDownloadButton';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
row: Row
|
row: Row
|
||||||
|
@ -19,13 +20,6 @@ interface IProps {
|
||||||
|
|
||||||
const BookDetailsCard: React.FC<IProps> = (props) => {
|
const BookDetailsCard: React.FC<IProps> = (props) => {
|
||||||
|
|
||||||
const downloadLinkFromIPFS = (gateway: string, book: Book) => {
|
|
||||||
return (
|
|
||||||
`https://${gateway}/ipfs/${book.ipfs_cid}?filename=` +
|
|
||||||
encodeURIComponent(`${book.title}_${book.author}.${book.extension}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { row } = props
|
const { row } = props
|
||||||
|
@ -44,12 +38,32 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
|
||||||
ipfs_cid
|
ipfs_cid
|
||||||
} = row.original;
|
} = row.original;
|
||||||
|
|
||||||
|
const searchOnAnnasArchive = () => {
|
||||||
|
return (`https://annas-archive.org/search?q=` + encodeURIComponent(`${title} ${author}`));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card mt={{ base: 1, md: 2 }} mb={{ base: 2, md: 4 }} mx={{ base: 4, md: 8 }}>
|
<Card mt={{ base: 1, md: 2 }} mb={{ base: 2, md: 4 }} mx={{ base: 4, md: 8 }}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Heading as="h3" fontSize="xl">
|
<Flex
|
||||||
{title}
|
align="center"
|
||||||
</Heading>
|
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
|
||||||
|
justify="space-between"
|
||||||
|
gap={{ base: '4', lg: '2' }}
|
||||||
|
>
|
||||||
|
<Heading as="h3" fontSize={['xl', '2xl', '2xl']} whiteSpace="break-spaces" minW="0">
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button
|
||||||
|
as={ExternalLink}
|
||||||
|
minWidth="unset"
|
||||||
|
href={searchOnAnnasArchive()}
|
||||||
|
>
|
||||||
|
{t('input.anna_archive')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
@ -87,9 +101,12 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter flexDirection="column">
|
<CardFooter flexDirection="column">
|
||||||
|
|
||||||
<SimpleGrid columns={{ sm: 2, md: 3, lg: 4, xl: 5 }} spacing={{ base: 2, md: 4 }}>
|
<SimpleGrid columns={{ sm: 2, md: 3, lg: 4, xl: 5 }} spacing={{ base: 2, md: 4 }}>
|
||||||
{ipfsGateways.map((gateway) => (
|
|
||||||
|
<IpfsDownloadButton book={row.original as Book} onlyIcon={false}></IpfsDownloadButton>
|
||||||
|
|
||||||
|
{/* {ipfsGateways.map((gateway) => (
|
||||||
<Button
|
<Button
|
||||||
as={ExternalLink}
|
as={ExternalLink}
|
||||||
href={downloadLinkFromIPFS(gateway, row.original)}
|
href={downloadLinkFromIPFS(gateway, row.original)}
|
||||||
|
@ -98,11 +115,11 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
|
||||||
>
|
>
|
||||||
{gateway}
|
{gateway}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))} */}
|
||||||
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{/* <Flex><Text fontWeight={'bold'}>{t('disclaimer.nolink_warning')}</Text></Flex> */}
|
<Flex><Text mt={2} mb={-2} fontSize={'2xs'} fontStyle={'italic'} fontWeight={'light'}>{t('disclaimer.broken_link')}</Text></Flex>
|
||||||
|
|
||||||
<Flex justify="flex-end">
|
<Flex justify="flex-end">
|
||||||
<Button
|
<Button
|
||||||
|
@ -117,7 +134,7 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button, useToast } from '@chakra-ui/react';
|
||||||
|
import { DownloadIcon } from '@chakra-ui/icons';
|
||||||
|
import { Book } from '../scripts/searcher';
|
||||||
|
import autoDownload from '../scripts/download';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
export interface IpfsDownloadButtonProps {
|
||||||
|
book: Book;
|
||||||
|
onlyIcon: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IpfsDownloadButton: React.FC<IpfsDownloadButtonProps> = ({ book, onlyIcon }) => {
|
||||||
|
const toast = useToast();
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState(-1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key="input.download"
|
||||||
|
w="100%"
|
||||||
|
zIndex={111}
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="blue"
|
||||||
|
leftIcon={onlyIcon ? undefined : <DownloadIcon />}
|
||||||
|
isLoading={downloadProgress > -1}
|
||||||
|
loadingText={`${downloadProgress}%`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
autoDownload(book, toast, setDownloadProgress);
|
||||||
|
}}
|
||||||
|
style={{ fontVariantNumeric: 'tabular-nums' }}
|
||||||
|
>
|
||||||
|
{onlyIcon ? <DownloadIcon /> : t('input.download')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IpfsDownloadButton;
|
|
@ -19,7 +19,6 @@ import { useTranslation } from 'react-i18next';
|
||||||
import SearchLanguage from './SearchLanguage';
|
import SearchLanguage from './SearchLanguage';
|
||||||
import { MEDIA_QUERY_DESKTOP_STARTS, MEDIA_QUERY_MOBILE_ENDS } from '../constants/mediaquery';
|
import { MEDIA_QUERY_DESKTOP_STARTS, MEDIA_QUERY_MOBILE_ENDS } from '../constants/mediaquery';
|
||||||
import CopyToClipboardButton from './CopyToClipboardButton';
|
import CopyToClipboardButton from './CopyToClipboardButton';
|
||||||
import { getJsonArchive } from '../scripts/searcher-browser';
|
|
||||||
|
|
||||||
function constructQuery(parts: Record<string, string>): string {
|
function constructQuery(parts: Record<string, string>): string {
|
||||||
return Object.keys(parts)
|
return Object.keys(parts)
|
||||||
|
@ -71,13 +70,6 @@ const Search: React.FC<SearchProps> = ({ setBooks }) => {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const jsonArchive = await getJsonArchive() as Book[]
|
|
||||||
setBooksFromJsonArchive(jsonArchive)
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleLanguageChange = (language: string) => {
|
const handleLanguageChange = (language: string) => {
|
||||||
if (language == 'input') {
|
if (language == 'input') {
|
||||||
setShowLanguageDropdown(false)
|
setShowLanguageDropdown(false)
|
||||||
|
|
|
@ -9,7 +9,9 @@
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"select_language": "(Select language...)"
|
"select_language": "(Select language...)",
|
||||||
|
"download": "Download",
|
||||||
|
"anna_archive": "Anna's Archive"
|
||||||
},
|
},
|
||||||
"book": {
|
"book": {
|
||||||
"id": "zlib/libgen id",
|
"id": "zlib/libgen id",
|
||||||
|
@ -63,107 +65,8 @@
|
||||||
"input": "Input..."
|
"input": "Input..."
|
||||||
},
|
},
|
||||||
"disclaimer": {
|
"disclaimer": {
|
||||||
"nolink_warning": "WARNING: This platform does not host any kind of link to copyrighted material."
|
"nolink_warning": "WARNING: This platform does not host any kind of link to copyrighted material.",
|
||||||
}
|
"broken_link": "WARNING: Links might be broken. If download doesn't start, try with Anna's Archive"
|
||||||
}
|
|
||||||
},
|
|
||||||
"zh-CN": {
|
|
||||||
"translation": {
|
|
||||||
"nav": {
|
|
||||||
"repository": "GitHub 仓库",
|
|
||||||
"toggle_dark": "切换到暗黑模式",
|
|
||||||
"toggle_light": "切换到亮色模式",
|
|
||||||
"toggle_language": "切换语言"
|
|
||||||
},
|
|
||||||
"input": {
|
|
||||||
"clear": "清空"
|
|
||||||
},
|
|
||||||
"book": {
|
|
||||||
"id": "zlib/libgen id",
|
|
||||||
"title": "书名",
|
|
||||||
"author": "作者",
|
|
||||||
"publisher": "出版社",
|
|
||||||
"extension": "扩展名",
|
|
||||||
"filesize": "文件大小",
|
|
||||||
"language": "语言",
|
|
||||||
"year": "年份",
|
|
||||||
"pages": "页数",
|
|
||||||
"isbn": "ISBN",
|
|
||||||
"ipfs_cid": "IPFS CID",
|
|
||||||
"unknown": "未知"
|
|
||||||
},
|
|
||||||
"table": {
|
|
||||||
"sort_asc": "升序排序",
|
|
||||||
"sort_desc": "降序排序",
|
|
||||||
"not_sorted": "未排序",
|
|
||||||
"filter": "过滤",
|
|
||||||
"no_data": "无数据",
|
|
||||||
"first_page": "第一页",
|
|
||||||
"last_page": "最后一页",
|
|
||||||
"next_page": "下一页",
|
|
||||||
"previous_page": "上一页",
|
|
||||||
"page": "第 {{page}} 页",
|
|
||||||
"collapse": "收起"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"complex": "复杂搜索"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": "设置",
|
|
||||||
"ipfs_gateways": "IPFS 网关",
|
|
||||||
"ipfs_gateways_help": "IPFS 网关列表,一行一个",
|
|
||||||
"cancel": "取消",
|
|
||||||
"save": "保存"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr": {
|
|
||||||
"translation": {
|
|
||||||
"nav": {
|
|
||||||
"repository": "Dépôt GitHub",
|
|
||||||
"toggle_dark": "Basculer en mode sombre",
|
|
||||||
"toggle_light": "Basculer en mode clair",
|
|
||||||
"toggle_language": "Basculer la langue"
|
|
||||||
},
|
|
||||||
"input": {
|
|
||||||
"clear": "Effacer"
|
|
||||||
},
|
|
||||||
"book": {
|
|
||||||
"id": "ID zlib/libgen",
|
|
||||||
"title": "Titre",
|
|
||||||
"author": "Auteur",
|
|
||||||
"publisher": "Éditeur",
|
|
||||||
"extension": "Extension",
|
|
||||||
"filesize": "Taille du fichier",
|
|
||||||
"language": "Langue",
|
|
||||||
"year": "Année",
|
|
||||||
"pages": "Pages",
|
|
||||||
"isbn": "ISBN",
|
|
||||||
"ipfs_cid": "CID IPFS",
|
|
||||||
"unknown": "Inconnu"
|
|
||||||
},
|
|
||||||
"table": {
|
|
||||||
"sort_asc": "Trier par ordre croissant",
|
|
||||||
"sort_desc": "Trier par ordre décroissant",
|
|
||||||
"not_sorted": "Non trié",
|
|
||||||
"filter": "Filtrer",
|
|
||||||
"no_data": "Aucune donnée",
|
|
||||||
"first_page": "Première page",
|
|
||||||
"last_page": "Dernière page",
|
|
||||||
"next_page": "Page suivante",
|
|
||||||
"previous_page": "Page précédente",
|
|
||||||
"page": "Page {{page}}",
|
|
||||||
"collapse": "Replier"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"complex": "Recherche complexe"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": "Paramètres",
|
|
||||||
"ipfs_gateways": "Passerelles IPFS",
|
|
||||||
"ipfs_gateways_help": "Liste des passerelles IPFS, saut de ligne",
|
|
||||||
"cancel": "Annuler",
|
|
||||||
"save": "Enregistrer"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -177,7 +80,9 @@
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"clear": "pulisci",
|
"clear": "pulisci",
|
||||||
"select_language": "(Seleziona lingua...)"
|
"select_language": "(Seleziona lingua...)",
|
||||||
|
"download": "Download",
|
||||||
|
"anna_archive": "Anna's Archive"
|
||||||
},
|
},
|
||||||
"book": {
|
"book": {
|
||||||
"id": "ID zlib/libgen",
|
"id": "ID zlib/libgen",
|
||||||
|
@ -231,7 +136,8 @@
|
||||||
"input": "Scrivi..."
|
"input": "Scrivi..."
|
||||||
},
|
},
|
||||||
"disclaimer": {
|
"disclaimer": {
|
||||||
"nolink_warning": "IMPORTANTE: Questa piattaforma non ospita nessun tipo di link a materiale protetto da copyright."
|
"nolink_warning": "IMPORTANTE: Questa piattaforma non ospita nessun tipo di link a materiale protetto da copyright.",
|
||||||
|
"broken_link": "IMPORTANTE: I link potrebbero non funzionare. Se il download non parte, provare con Anna's Archive."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
import type { Book } from './searcher';
|
||||||
|
import getIpfsGateways, { getDownloadLinkFromIPFS, ipfsGateways } from './ipfs';
|
||||||
|
import axios, { AxiosProgressEvent, AxiosResponse } from 'axios';
|
||||||
|
import fileDownload from 'js-file-download';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
export default async function autoDownload(book: Book, toast: any, setDownloadProgress: any) {
|
||||||
|
const filename = `${book.title}_${book.author}.${book.extension}`;
|
||||||
|
toast({
|
||||||
|
title: `${filename} ${t('download_start')}!`,
|
||||||
|
status: 'info',
|
||||||
|
position: 'bottom-right',
|
||||||
|
isClosable: true,
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Download: ', book);
|
||||||
|
var gateways = ipfsGateways;
|
||||||
|
gateways = gateways.filter(function (item, pos) {
|
||||||
|
return gateways.indexOf(item) == pos;
|
||||||
|
});
|
||||||
|
console.log('Try gateways:', gateways);
|
||||||
|
const controllerMap = new Map();
|
||||||
|
var fastedProgress = 0;
|
||||||
|
setDownloadProgress(fastedProgress.toFixed(2));
|
||||||
|
Promise.any(
|
||||||
|
gateways.map((gateway) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
controllerMap.set(gateway, controller);
|
||||||
|
return axios
|
||||||
|
.get(getDownloadLinkFromIPFS(gateway, book), {
|
||||||
|
signal: controller.signal,
|
||||||
|
withCredentials: false,
|
||||||
|
responseType: 'blob',
|
||||||
|
onDownloadProgress: (e: AxiosProgressEvent) => {
|
||||||
|
console.log('Download Progress: ', gateway, e);
|
||||||
|
const myProgress = e.progress! * 100;
|
||||||
|
const bar = 10;
|
||||||
|
if (fastedProgress > bar && myProgress < bar) {
|
||||||
|
controllerMap.get(gateway).abort();
|
||||||
|
}
|
||||||
|
if (myProgress > fastedProgress && myProgress != 100) {
|
||||||
|
fastedProgress = myProgress;
|
||||||
|
setDownloadProgress(fastedProgress.toFixed(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then((resp: AxiosResponse) => {
|
||||||
|
controllerMap.forEach((c) => c.abort());
|
||||||
|
fileDownload(resp.data, filename);
|
||||||
|
toast({
|
||||||
|
title: `${filename} ${t('download_success')}!`,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
isClosable: true,
|
||||||
|
duration: 6000
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast({
|
||||||
|
title: `${filename} ${t('download_failed')}!`,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
isClosable: true,
|
||||||
|
duration: 6000
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setDownloadProgress(-1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadBookData = async function (book: Book, signal: AbortSignal) {
|
||||||
|
var gateways = await getIpfsGateways();
|
||||||
|
gateways = gateways.filter(function (item, pos) {
|
||||||
|
return gateways.indexOf(item) == pos;
|
||||||
|
});
|
||||||
|
const controllerMap = new Map();
|
||||||
|
var fastedProgress = 0;
|
||||||
|
return Promise.any(
|
||||||
|
gateways.map((gateway) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
controllerMap.set(gateway, controller);
|
||||||
|
return axios
|
||||||
|
.get(getDownloadLinkFromIPFS(gateway, book), {
|
||||||
|
signal: anySignal([controller.signal, signal]),
|
||||||
|
withCredentials: false,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
onDownloadProgress: (e: AxiosProgressEvent) => {
|
||||||
|
console.log('Download Progress: ', gateway, e);
|
||||||
|
const myProgress = e.progress! * 100;
|
||||||
|
const bar = 10;
|
||||||
|
if (fastedProgress > bar && myProgress < bar) {
|
||||||
|
controllerMap.get(gateway).abort();
|
||||||
|
}
|
||||||
|
if (myProgress > fastedProgress && myProgress != 100) {
|
||||||
|
fastedProgress = myProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then((resp: AxiosResponse) => {
|
||||||
|
return resp.data;
|
||||||
|
})
|
||||||
|
.catch()
|
||||||
|
.finally(() => {
|
||||||
|
controllerMap.forEach((c) => c.abort());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { downloadBookData };
|
||||||
|
|
||||||
|
function anySignal(signals: AbortSignal[]) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
function onAbort() {
|
||||||
|
controller.abort();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
for (const signal of signals) {
|
||||||
|
signal.removeEventListener('abort', onAbort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const signal of signals) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
onAbort();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
signal.addEventListener('abort', onAbort);
|
||||||
|
}
|
||||||
|
|
||||||
|
return controller.signal;
|
||||||
|
}
|
|
@ -1,14 +1,18 @@
|
||||||
|
import { Book } from './searcher';
|
||||||
|
|
||||||
interface TauriConfig {
|
interface TauriConfig {
|
||||||
index_dir: string;
|
index_dir: string;
|
||||||
ipfs_gateways: string[];
|
ipfs_gateways: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ipfsGateways: string[] = [
|
export const ipfsGateways: string[] = [
|
||||||
'cloudflare-ipfs.com',
|
// 'cloudflare-ipfs.com',
|
||||||
'ipfs.2read.net',
|
// 'dweb.link',
|
||||||
'dweb.link',
|
// 'ipfs.io',
|
||||||
'ipfs.io',
|
// 'gateway.pinata.cloud',
|
||||||
'gateway.pinata.cloud'
|
// 'nftstorage.link'
|
||||||
|
'ipfs.copyriot.xyz',
|
||||||
|
'ipfs2.copyriot.xyz'
|
||||||
];
|
];
|
||||||
|
|
||||||
export default async function getIpfsGateways() {
|
export default async function getIpfsGateways() {
|
||||||
|
@ -28,4 +32,11 @@ export default async function getIpfsGateways() {
|
||||||
|
|
||||||
export function parseIpfsGateways(text: string) {
|
export function parseIpfsGateways(text: string) {
|
||||||
return text.split('\n').filter(g => g.length);
|
return text.split('\n').filter(g => g.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDownloadLinkFromIPFS(gateway: string, book: Book) {
|
||||||
|
return (
|
||||||
|
`https://${gateway}/ipfs/${book.ipfs_cid}?filename=` +
|
||||||
|
encodeURIComponent(`${book.title}_${book.author}.${book.extension}`)
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,39 +8,6 @@ const http = axios.create({
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
const storj = axios.create({
|
|
||||||
baseURL: import.meta.env.VITE_STORJ_JSON_URL,
|
|
||||||
timeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getJsonArchive = async () => {
|
|
||||||
//@ts-ignore
|
|
||||||
if (!window[JSON_ARCHIVE_WINDOW_KEY]) {
|
|
||||||
|
|
||||||
const response = await storj.get(``);
|
|
||||||
|
|
||||||
if (response.status == 200) {
|
|
||||||
//@ts-ignore
|
|
||||||
window[JSON_ARCHIVE_WINDOW_KEY] = response.data as Book[];
|
|
||||||
} else {
|
|
||||||
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
window[JSON_ARCHIVE_WINDOW_KEY] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
return window[JSON_ARCHIVE_WINDOW_KEY] as Book[];
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
//@ts-ignore
|
|
||||||
return window[JSON_ARCHIVE_WINDOW_KEY] as Book[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default async function search(query: string, limit: number) {
|
export default async function search(query: string, limit: number) {
|
||||||
const response = await http.get(`search?limit=${limit}&query=${query}`);
|
const response = await http.get(`search?limit=${limit}&query=${query}`);
|
||||||
return response.data.books as Book[];
|
return response.data.books as Book[];
|
||||||
|
|
Loading…
Reference in New Issue