add concurrent dl button and links to copyriot gateways

This commit is contained in:
lamacchinadesiderante 2023-12-30 17:51:33 +01:00
parent 16c7e6bd35
commit 1218b60224
13 changed files with 1410 additions and 1247 deletions

View File

@ -6,9 +6,9 @@ Un motore di ricerca libri basato su protocollo IPFS.
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
@ -33,7 +33,3 @@ npm install
npm run build
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.

View File

@ -4,14 +4,6 @@ services:
zlib:
image: lamacchinadesiderante/millelibri:latest
# image: millelibri:v0.4
# image: millelibri
# build:
# context: .
# dockerfile: ./Dockerfile
restart: always
ports:

View File

@ -1,3 +1,2 @@
# .env.production
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'

View File

@ -1,3 +1,2 @@
# .env.production
VITE_BACKEND_BASE_API = ''
VITE_STORJ_JSON_URL = 'https://link.storjshare.io/juowot6xaa2rz4vjqeal2uo44jka/millelibri%2Findex%2Fjson%2Fmillelibri.json?download=1'

View File

@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "v0.7.0",
"version": "v0.8.0",
"type": "module",
"repository": "https://git.lamacchinadesiderante.org/lamacchinadesiderante/millelibri",
"scripts": {
@ -13,6 +13,7 @@
"@chakra-ui/react": "^2.4.6",
"@chakra-ui/skip-nav": "^2.0.13",
"@chakra-ui/system": "^2.3.7",
"@chakra-ui/icons": "^2.1.1",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@tanstack/react-table": "^8.7.4",
@ -24,6 +25,7 @@
"framer-motion": "^7.10.3",
"i18next": "^22.4.6",
"i18next-browser-languagedetector": "^7.0.1",
"js-file-download": "^0.4.12",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",

File diff suppressed because it is too large Load Diff

View File

@ -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';
@ -12,6 +12,7 @@ import { filesize as formatFileSize } from 'filesize';
import { TbChevronUp } from 'react-icons/tb';
import ExternalLink from './ExternalLink';
import Description from './Description';
import IpfsDownloadButton from './IpfsDownloadButton';
interface IProps {
row: Row
@ -19,13 +20,6 @@ interface IProps {
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 { row } = props
@ -44,12 +38,32 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
ipfs_cid
} = row.original;
const searchOnAnnasArchive = () => {
return (`https://annas-archive.org/search?q=` + encodeURIComponent(`${title} ${author}`));
}
return (
<Card mt={{ base: 1, md: 2 }} mb={{ base: 2, md: 4 }} mx={{ base: 4, md: 8 }}>
<CardHeader>
<Heading as="h3" fontSize="xl">
{title}
</Heading>
<Flex
align="center"
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>
<Divider />
<CardBody>
@ -89,7 +103,10 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
<CardFooter flexDirection="column">
<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
as={ExternalLink}
href={downloadLinkFromIPFS(gateway, row.original)}
@ -98,11 +115,11 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
>
{gateway}
</Button>
))}
))} */}
</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">
<Button
@ -117,7 +134,7 @@ const BookDetailsCard: React.FC<IProps> = (props) => {
</Button>
</Flex>
</CardFooter>
</Card>
</Card >
);
};

View File

@ -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;

View File

@ -19,7 +19,6 @@ import { useTranslation } from 'react-i18next';
import SearchLanguage from './SearchLanguage';
import { MEDIA_QUERY_DESKTOP_STARTS, MEDIA_QUERY_MOBILE_ENDS } from '../constants/mediaquery';
import CopyToClipboardButton from './CopyToClipboardButton';
import { getJsonArchive } from '../scripts/searcher-browser';
function constructQuery(parts: Record<string, string>): string {
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) => {
if (language == 'input') {
setShowLanguageDropdown(false)

View File

@ -9,7 +9,9 @@
},
"input": {
"clear": "Clear",
"select_language": "(Select language...)"
"select_language": "(Select language...)",
"download": "Download",
"anna_archive": "Anna's Archive"
},
"book": {
"id": "zlib/libgen id",
@ -63,107 +65,8 @@
"input": "Input..."
},
"disclaimer": {
"nolink_warning": "WARNING: This platform does not host any kind of link to copyrighted material."
}
}
},
"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"
"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"
}
}
},
@ -177,7 +80,9 @@
},
"input": {
"clear": "pulisci",
"select_language": "(Seleziona lingua...)"
"select_language": "(Seleziona lingua...)",
"download": "Download",
"anna_archive": "Anna's Archive"
},
"book": {
"id": "ID zlib/libgen",
@ -231,7 +136,8 @@
"input": "Scrivi..."
},
"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."
}
}
}

View File

@ -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;
}

View File

@ -1,14 +1,18 @@
import { Book } from './searcher';
interface TauriConfig {
index_dir: string;
ipfs_gateways: string[];
}
export const ipfsGateways: string[] = [
'cloudflare-ipfs.com',
'ipfs.2read.net',
'dweb.link',
'ipfs.io',
'gateway.pinata.cloud'
// 'cloudflare-ipfs.com',
// 'dweb.link',
// 'ipfs.io',
// 'gateway.pinata.cloud',
// 'nftstorage.link'
'ipfs.copyriot.xyz',
'ipfs2.copyriot.xyz'
];
export default async function getIpfsGateways() {
@ -29,3 +33,10 @@ export default async function getIpfsGateways() {
export function parseIpfsGateways(text: string) {
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}`)
);
}

View File

@ -8,39 +8,6 @@ const http = axios.create({
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) {
const response = await http.get(`search?limit=${limit}&query=${query}`);
return response.data.books as Book[];