Skip to content

Commit

Permalink
🚸 Extract Epub metadata for ISCN registration (#408)
Browse files Browse the repository at this point in the history
* ✨ Get epub metadata & coverInfo

* ✨ Add epubMetadata to thumbnail field

* 🚸  Provide additional information for sameAs

* ✨ Add ISBN field for Book

* 🐛 Fix read fileType undefined

* 🚸 Halt further execution on error

* 🎨 Improve naming & Enhance type definitions

* 🎨 Improve logic for fetching cover file

* 🎨 Store arweaveId without adding prefix

* 🎨 Store epubMetadata after processing

* 🚸 Display only epub & pdf in sameAs

* ✏️ Rename formatUrlOptions to filteredUrlOptions

* 🥅 Guard empty arweaveId
  • Loading branch information
AuroraHuang22 authored Nov 6, 2023
1 parent bfc65c3 commit 7d87604
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 26 deletions.
38 changes: 37 additions & 1 deletion components/IscnRegisterForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,15 @@
:placeholder="$t('IscnRegisterForm.placeholder.url')"
/>
</FormField>
<FormField
v-if="type === 'Book'"
:label="$t('IscnRegisterForm.label.isbn')"
>
<TextField
v-model="isbn"
:placeholder="$t('IscnRegisterForm.placeholder.isbn')"
/>
</FormField>
<Divider class="my-[12px]" />
<FormField
:label="$t('IscnRegisterForm.label.type')"
Expand Down Expand Up @@ -594,6 +603,7 @@
<SameAsFieldList
:name="name"
:url-options="contentFingerprintLinks"
:file-records="fileRecords"
:current-list="sameAsList"
@onConfirm="confirmSameAsChange"
/>
Expand Down Expand Up @@ -740,6 +750,7 @@ export enum AuthorDialogType {
export default class IscnRegisterForm extends Vue {
@Prop({ default: [] }) readonly fileRecords!: any[]
@Prop({ default: [] }) readonly uploadArweaveList!: string[]
@Prop() readonly epubMetadata!: any | null
@Prop(String) readonly ipfsHash!: string
@Prop(String) readonly arweaveId!: string
Expand All @@ -759,7 +770,7 @@ export default class IscnRegisterForm extends Vue {
fileTypeOptions = [
'epub',
'pdf',
'mp3',
'audio',
'jpg',
'png',
]
Expand All @@ -784,8 +795,10 @@ export default class IscnRegisterForm extends Vue {
tags: string[] = []
sameAs: string[] = []
url: string = ''
isbn: string = ''
license: string = this.licenseOptions[0]
customLicense: string = ''
thumbnailUrl: string = ''
authorName: string = ''
authorUrl: string[] = []
authorWalletAddress: string[] = []
Expand Down Expand Up @@ -834,6 +847,7 @@ export default class IscnRegisterForm extends Vue {
currentAuthorDialogType: AuthorDialogType = AuthorDialogType.stakeholder
sameAsList: any = []
language: string = ''
get ipfsHashList() {
const list = []
Expand Down Expand Up @@ -987,6 +1001,7 @@ export default class IscnRegisterForm extends Vue {
tagsString: this.tagsString,
sameAs: this.formattedSameAsList,
url: this.url,
isbn: this.isbn,
exifInfo: this.exif.filter(file => file),
license: this.formattedLicense,
ipfsHash: this.ipfsHashList,
Expand All @@ -1001,6 +1016,8 @@ export default class IscnRegisterForm extends Vue {
likerIdsAddresses: this.likerIdsAddresses,
authorDescriptions: this.authorDescriptions,
contentFingerprints: this.customContentFingerprints,
inLanguage: this.language,
thumbnailUrl: this.thumbnailUrl,
}
}
Expand Down Expand Up @@ -1084,6 +1101,16 @@ export default class IscnRegisterForm extends Vue {
}
async mounted() {
if (this.epubMetadata) {
this.name = this.epubMetadata.title;
this.description = this.extractText(this.epubMetadata.description);
this.author.name = this.epubMetadata.author;
this.language = this.epubMetadata.language
this.tags = this.epubMetadata.tags
this.thumbnailUrl = this.formatArweave(this.epubMetadata.thumbnailUrl) as string
if (this.author.name) { this.authors.push(this.author) }
}
this.uploadStatus = 'loading'
// ISCN Fee needs Arweave fee to calculate
await this.calculateISCNFee()
Expand Down Expand Up @@ -1433,5 +1460,14 @@ export default class IscnRegisterForm extends Vue {
this.displayImageSrc = this.fileRecords[index].fileData
this.displayExifInfo = this.fileRecords[index].exifInfo
}
// eslint-disable-next-line class-methods-use-this
extractText(htmlString: string) {
if (!htmlString) return ''
const div = document.createElement('div');
div.innerHTML = htmlString;
div.innerHTML = div.innerHTML.replace(/<br\s*[/]?>/gi, "\n");
return div.textContent || div.innerText;
}
}
</script>
156 changes: 145 additions & 11 deletions components/IscnUploadForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ import { namespace } from 'vuex-class'
import exifr from 'exifr'
import Hash from 'ipfs-only-hash'
import BigNumber from 'bignumber.js'
import ePub from 'epubjs';
import { OfflineSigner } from '@cosmjs/proto-signing'
Expand Down Expand Up @@ -282,8 +283,6 @@ export default class UploadForm extends Vue {
string, { transactionHash?: string, arweaveId?: string }
>()
likerId: string = ''
error: string = ''
shouldShowAlert = false
Expand All @@ -292,6 +291,8 @@ export default class UploadForm extends Vue {
signDialogError = ''
balance = new BigNumber(0)
epubMetadataList: any[] = []
get formClasses() {
return [
'flex',
Expand Down Expand Up @@ -356,7 +357,7 @@ export default class UploadForm extends Vue {
case 'MISSING_SIGNER':
return this.$t('IscnRegisterForm.error.missingSigner')
default:
return ''
return this.error
}
}
Expand Down Expand Up @@ -442,6 +443,10 @@ export default class UploadForm extends Vue {
console.error(err)
}
}
if (file.type === 'application/epub+zip') {
// eslint-disable-next-line no-await-in-loop
await this.processEPub({ buffer: fileBytes, file })
}
}
} else {
this.isSizeExceeded = true
Expand All @@ -451,6 +456,109 @@ export default class UploadForm extends Vue {
}
}
async processEPub({ buffer, file }: { buffer: ArrayBuffer; file: File }) {
try {
const book = ePub(buffer)
await book.ready
const epubMetadata: any = {}
// Get metadata
const { metadata } = book.packaging
if (metadata) {
epubMetadata.epubFileName = file.name
epubMetadata.title = metadata.title
epubMetadata.author = metadata.creator
epubMetadata.language = this.formatLanguage(metadata.language)
epubMetadata.description = metadata.description
}
// Get tags
const opfFilePath = await (book.path as any).path
const opfContent = await book.archive.getText(opfFilePath)
const parser = new DOMParser()
const opfDocument = parser.parseFromString(opfContent, 'application/xml')
const dcSubjectElements = opfDocument.querySelectorAll(
'dc\\:subject, subject',
)
const subjects: string[] = []
dcSubjectElements.forEach((element) => {
const subject = element.textContent
if (subject) {
subjects.push(subject)
}
})
epubMetadata.tags = subjects
// Get cover file
const coverUrl = await book.coverUrl()
if (coverUrl) {
const response = await fetch(coverUrl)
const blobData = await response.blob()
if (blobData) {
const coverFile = new File(
[blobData],
`${metadata.title}_cover.jpeg`,
{
type: 'image/jpeg',
},
)
const fileBytes = (await fileToArrayBuffer(
coverFile,
)) as unknown as ArrayBuffer
if (fileBytes) {
const [
fileSHA256,
imageType,
ipfsHash,
// eslint-disable-next-line no-await-in-loop
] = await Promise.all([
digestFileSHA256(fileBytes),
readImageType(fileBytes),
Hash.of(Buffer.from(fileBytes)),
])
epubMetadata.ipfsHash = ipfsHash
const fileRecord: any = {
fileName: coverFile.name,
fileSize: coverFile.size,
fileType: coverFile.type,
fileBlob: coverFile,
ipfsHash,
fileSHA256,
isFileImage: !!imageType,
}
const reader = new FileReader()
reader.onload = (e) => {
if (!e.target) return
fileRecord.fileData = e.target.result as string
this.fileRecords.push(fileRecord)
}
reader.readAsDataURL(coverFile)
}
}
}
this.epubMetadataList.push(epubMetadata)
} catch (err) {
console.error(err)

Check warning on line 543 in components/IscnUploadForm.vue

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 16)

Unexpected console statement
}
}
// eslint-disable-next-line class-methods-use-this
formatLanguage(language: string) {
let formattedLanguage = '';
if (language) {
if (language.toLowerCase().startsWith('en')) {
formattedLanguage = 'en'
} else if (language.toLowerCase().startsWith('zh')) {
formattedLanguage = 'zh'
} else {
formattedLanguage = language
}
}
return formattedLanguage
}
onEnterURL() {
if (
!(
Expand All @@ -472,7 +580,13 @@ export default class UploadForm extends Vue {
}
handleDeleteFile(index: number) {
this.fileRecords.splice(index, 1)
const deletedFile = this.fileRecords[index];
this.fileRecords.splice(index, 1);
const indexToDelete = this.epubMetadataList.findIndex(item => item.ipfsHash === deletedFile.ipfsHashList);
if (indexToDelete !== -1) {
this.epubMetadataList.splice(indexToDelete, 1);
}
}
handleClickExifInfo(index: number) {
Expand Down Expand Up @@ -514,6 +628,10 @@ export default class UploadForm extends Vue {
}
if (arweaveId) {
this.sentArweaveTransactionInfo.set(ipfsHash, { transactionHash: '', arweaveId });
const metadata = this.epubMetadataList.find((data: any) => data.ipfsHash === ipfsHash)
if (metadata) {
metadata.thumbnailUrl = arweaveId;
}
}
if (!this.arweaveFeeTargetAddress) {
this.arweaveFeeTargetAddress = address;
Expand Down Expand Up @@ -589,19 +707,21 @@ export default class UploadForm extends Vue {
if (arweaveId) {
const uploadedData = this.sentArweaveTransactionInfo.get(records.ipfsHash) || {};
this.sentArweaveTransactionInfo.set(records.ipfsHash, { ...uploadedData, arweaveId });
if (tempRecord.fileName.includes('cover.jpeg')) {
const metadata = this.epubMetadataList.find((file: any) => file.ipfsHash === records.ipfsHash)
metadata.thumbnailUrl = arweaveId
}
this.$emit('arweaveUploaded', { arweaveId })
this.isOpenSignDialog = false
} else {
this.shouldShowAlert = true
this.errorMessage = this.$t('IscnRegisterForm.error.arweave') as string
this.$emit('handleContinue')
this.isOpenWarningSnackbar = true
this.error = this.$t('IscnRegisterForm.error.arweave') as string
throw new Error(this.error)
}
} catch (err) {
// TODO: Handle error
// eslint-disable-next-line no-console
console.error(err)
this.shouldShowAlert = true
this.errorMessage = (err as Error).toString()
throw new Error(err as string)
}
}
Expand Down Expand Up @@ -637,12 +757,26 @@ export default class UploadForm extends Vue {
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
this.isOpenWarningSnackbar = true
this.error = (error as Error).toString()
this.uploadStatus = '';
return
} finally {
this.uploadStatus = '';
}
const uploadArweaveIdList = Array.from(this.sentArweaveTransactionInfo.values()).map(entry => entry.arweaveId);
this.$emit('submit', { fileRecords: this.fileRecords, arweaveIds: uploadArweaveIdList })
this.fileRecords.forEach((record: any, index:number) => {
if (this.sentArweaveTransactionInfo.has(record.ipfsHash)) {
const arweaveId = this.sentArweaveTransactionInfo.get(
record.ipfsHash,
)?.arweaveId
if (arweaveId) {
this.fileRecords[index].arweaveId = arweaveId
}
}
})
this.$emit('submit', { fileRecords: this.fileRecords, arweaveIds: uploadArweaveIdList, epubMetadata: this.epubMetadataList[0] })
}
handleSignDialogClose() {
Expand Down
Loading

0 comments on commit 7d87604

Please sign in to comment.