Skip to content

Commit 63041b6

Browse files
authored
Merge pull request #4131 from manavagr1108/extract-metadata-for-h5p-cp
Implementation to extract metadata from H5P Content Package
2 parents d401e85 + 3f68c51 commit 63041b6

File tree

6 files changed

+153
-19
lines changed

6 files changed

+153
-19
lines changed

contentcuration/contentcuration/frontend/channelEdit/components/edit/EditModal.vue

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -489,13 +489,18 @@
489489
},
490490
createNodesFromUploads(fileUploads) {
491491
fileUploads.forEach((file, index) => {
492-
const title = file.original_filename
493-
.split('.')
494-
.slice(0, -1)
495-
.join('.');
492+
let title;
493+
if (file.metadata.title) {
494+
title = file.metadata.title;
495+
} else {
496+
title = file.original_filename
497+
.split('.')
498+
.slice(0, -1)
499+
.join('.');
500+
}
496501
this.createNode(
497502
FormatPresets.has(file.preset) && FormatPresets.get(file.preset).kind_id,
498-
{ title }
503+
{ title, ...file.metadata }
499504
).then(newNodeId => {
500505
if (index === 0) {
501506
this.selected = [newNodeId];

contentcuration/contentcuration/frontend/shared/vuex/file/__tests__/module.spec.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import JSZip from 'jszip';
2+
import { getH5PMetadata } from '../utils';
13
import storeFactory from 'shared/vuex/baseStore';
24
import { File, injectVuexStore } from 'shared/data/resources';
35
import client from 'shared/client';
@@ -19,6 +21,27 @@ const testFile = {
1921

2022
const userId = 'some user';
2123

24+
function get_metadata_file(data) {
25+
const manifest = {
26+
h5p: '1.0',
27+
mainLibrary: 'content',
28+
libraries: [
29+
{
30+
machineName: 'content',
31+
majorVersion: 1,
32+
minorVersion: 0,
33+
},
34+
],
35+
content: {
36+
library: 'content',
37+
},
38+
...data,
39+
};
40+
const manifestBlob = new Blob([JSON.stringify(manifest, null, 2)], { type: 'application/json' });
41+
const manifestFile = new global.File([manifestBlob], 'h5p.json', { type: 'application/json' });
42+
return manifestFile;
43+
}
44+
2245
describe('file store', () => {
2346
let store;
2447
let id;
@@ -122,5 +145,53 @@ describe('file store', () => {
122145
});
123146
});
124147
});
148+
describe('H5P content file extract metadata', () => {
149+
it('getH5PMetadata should check for h5p.json file', () => {
150+
const zip = new JSZip();
151+
return zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) {
152+
await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toThrowError(
153+
'h5p.json not found in the H5P file.'
154+
);
155+
});
156+
});
157+
it('getH5PMetadata should exract metadata from h5p.json', async () => {
158+
const manifestFile = get_metadata_file({ title: 'Test file' });
159+
const zip = new JSZip();
160+
zip.file('h5p.json', manifestFile);
161+
await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) {
162+
await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toEqual({
163+
title: 'Test file',
164+
});
165+
});
166+
});
167+
it('getH5PMetadata should not extract und language', async () => {
168+
const manifestFile = get_metadata_file({ title: 'Test file', language: 'und' });
169+
const zip = new JSZip();
170+
zip.file('h5p.json', manifestFile);
171+
await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) {
172+
await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toEqual({
173+
title: 'Test file',
174+
});
175+
});
176+
});
177+
it('getH5PMetadata should exract metadata from h5p.json', async () => {
178+
const manifestFile = get_metadata_file({
179+
title: 'Test file',
180+
language: 'en',
181+
authors: 'author1',
182+
license: 'license1',
183+
});
184+
const zip = new JSZip();
185+
zip.file('h5p.json', manifestFile);
186+
await zip.generateAsync({ type: 'blob' }).then(async function(h5pBlob) {
187+
await expect(Promise.resolve(getH5PMetadata(h5pBlob))).resolves.toEqual({
188+
title: 'Test file',
189+
language: 'en',
190+
author: 'author1',
191+
license: 'license1',
192+
});
193+
});
194+
});
195+
});
125196
});
126197
});

contentcuration/contentcuration/frontend/shared/vuex/file/actions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export function uploadFile(context, { file, preset = null } = {}) {
198198
}); // End get upload url
199199
})
200200
.then(data => {
201+
data.file.metadata = metadata;
201202
const fileObject = {
202203
...data.file,
203204
loaded: 0,

contentcuration/contentcuration/frontend/shared/vuex/file/utils.js

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import SparkMD5 from 'spark-md5';
2+
import JSZip from 'jszip';
23
import { FormatPresetsList, FormatPresetsNames } from 'shared/leUtils/FormatPresets';
34

45
const BLOB_SLICE = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
@@ -7,8 +8,10 @@ const MEDIA_PRESETS = [
78
FormatPresetsNames.AUDIO,
89
FormatPresetsNames.HIGH_RES_VIDEO,
910
FormatPresetsNames.LOW_RES_VIDEO,
11+
FormatPresetsNames.H5P,
1012
];
1113
const VIDEO_PRESETS = [FormatPresetsNames.HIGH_RES_VIDEO, FormatPresetsNames.LOW_RES_VIDEO];
14+
const H5P_PRESETS = [FormatPresetsNames.H5P];
1215

1316
export function getHash(file) {
1417
return new Promise((resolve, reject) => {
@@ -61,6 +64,40 @@ export function storageUrl(checksum, file_format) {
6164
return `/content/storage/${checksum[0]}/${checksum[1]}/${checksum}.${file_format}`;
6265
}
6366

67+
export async function getH5PMetadata(fileInput) {
68+
const zip = new JSZip();
69+
const metadata = {};
70+
return zip
71+
.loadAsync(fileInput)
72+
.then(function(zip) {
73+
const h5pJson = zip.file('h5p.json');
74+
if (h5pJson) {
75+
return h5pJson.async('text');
76+
} else {
77+
throw new Error('h5p.json not found in the H5P file.');
78+
}
79+
})
80+
.then(function(h5pContent) {
81+
const data = JSON.parse(h5pContent);
82+
if (Object.prototype.hasOwnProperty.call(data, 'title')) {
83+
metadata.title = data['title'];
84+
}
85+
if (Object.prototype.hasOwnProperty.call(data, 'language') && data['language'] !== 'und') {
86+
metadata.language = data['language'];
87+
}
88+
if (Object.prototype.hasOwnProperty.call(data, 'authors')) {
89+
metadata.author = data['authors'];
90+
}
91+
if (Object.prototype.hasOwnProperty.call(data, 'license')) {
92+
metadata.license = data['license'];
93+
}
94+
return metadata;
95+
})
96+
.catch(function(error) {
97+
return error;
98+
});
99+
}
100+
64101
/**
65102
* @param {{name: String, preset: String}} file
66103
* @param {String|null} preset
@@ -85,24 +122,33 @@ export function extractMetadata(file, preset = null) {
85122
return Promise.resolve(metadata);
86123
}
87124

125+
const isH5P = H5P_PRESETS.includes(metadata.preset);
126+
88127
// Extract additional media metadata
89128
const isVideo = VIDEO_PRESETS.includes(metadata.preset);
90129

91130
return new Promise(resolve => {
92-
const mediaElement = document.createElement(isVideo ? 'video' : 'audio');
93-
// Add a listener to read the metadata once it has loaded.
94-
mediaElement.addEventListener('loadedmetadata', () => {
95-
metadata.duration = Math.floor(mediaElement.duration);
96-
// Override preset based off video resolution
97-
if (isVideo) {
98-
metadata.preset =
99-
mediaElement.videoHeight >= 720
100-
? FormatPresetsNames.HIGH_RES_VIDEO
101-
: FormatPresetsNames.LOW_RES_VIDEO;
102-
}
131+
if (isH5P) {
132+
getH5PMetadata(file).then(data => {
133+
if (data.constructor !== Error) Object.assign(metadata, ...data);
134+
});
103135
resolve(metadata);
104-
});
105-
// Set the src url on the media element
106-
mediaElement.src = URL.createObjectURL(file);
136+
} else {
137+
const mediaElement = document.createElement(isVideo ? 'video' : 'audio');
138+
// Add a listener to read the metadata once it has loaded.
139+
mediaElement.addEventListener('loadedmetadata', () => {
140+
metadata.duration = Math.floor(mediaElement.duration);
141+
// Override preset based off video resolution
142+
if (isVideo) {
143+
metadata.preset =
144+
mediaElement.videoHeight >= 720
145+
? FormatPresetsNames.HIGH_RES_VIDEO
146+
: FormatPresetsNames.LOW_RES_VIDEO;
147+
}
148+
resolve(metadata);
149+
});
150+
// Set the src url on the media element
151+
mediaElement.src = URL.createObjectURL(file);
152+
}
107153
});
108154
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"intl": "1.2.5",
7171
"jquery": "^2.2.4",
7272
"jspdf": "https://github.com/parallax/jsPDF.git#b7a1d8239c596292ce86dafa77f05987bcfa2e6e",
73+
"jszip": "^3.10.1",
7374
"kolibri-constants": "^0.1.41",
7475
"kolibri-design-system": "https://github.com/learningequality/kolibri-design-system#e9a2ff34716bb6412fe99f835ded5b17345bab94",
7576
"lodash": "^4.17.21",

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8960,6 +8960,16 @@ jsprim@^1.2.2:
89608960
json-schema "0.4.0"
89618961
verror "1.10.0"
89628962

8963+
jszip@^3.10.1:
8964+
version "3.10.1"
8965+
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
8966+
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
8967+
dependencies:
8968+
lie "~3.3.0"
8969+
pako "~1.0.2"
8970+
readable-stream "~2.3.6"
8971+
setimmediate "^1.0.5"
8972+
89638973
jszip@^3.7.1:
89648974
version "3.10.0"
89658975
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.0.tgz#faf3db2b4b8515425e34effcdbb086750a346061"

0 commit comments

Comments
 (0)