Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support "downloading" a content pack without an internet connection #905

Merged
merged 3 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 2 additions & 21 deletions kolibri_explore_plugin/assets/src/components/DiscoveryNavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
import { mapMutations } from 'vuex';
import { assets } from 'ek-components';
import commonExploreStrings from '../views/commonExploreStrings';
import isOfflineMixin from '../mixins/isOfflineMixin';
import { PageNames } from '../constants';

export default {
Expand All @@ -88,12 +89,7 @@
MessageReplyTextOutlineIcon,
ArrowDownIcon,
},
mixins: [commonExploreStrings],
data() {
return {
isOffline: false,
};
},
mixins: [commonExploreStrings, isOfflineMixin],
computed: {
logo() {
return assets.EndlessLogo;
Expand All @@ -116,15 +112,6 @@
return !!plugin_data.androidApplicationId && !!plugin_data.windowsApplicationId;
},
},
created() {
this.isOffline = !navigator.onLine;
window.addEventListener('offline', this.onOffline);
window.addEventListener('online', this.onOnline);
},
destroyed() {
window.removeEventListener('offline', this.onOffline);
window.removeEventListener('online', this.onOnline);
},
methods: {
...mapMutations({
setSearchResult: 'topicsRoot/SET_SEARCH_RESULT',
Expand Down Expand Up @@ -158,12 +145,6 @@
currentIsSearch() {
return this.$route.name === PageNames.SEARCH;
},
onOffline() {
this.isOffline = true;
},
onOnline() {
this.isOffline = false;
},
onLogoClick(event) {
if (event.ctrlKey) {
this.$store.commit('topicsRoot/SET_SHOW_SIDE_NAV', true);
Expand Down
24 changes: 24 additions & 0 deletions kolibri_explore_plugin/assets/src/mixins/isOfflineMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default {
data() {
return {
isOffline: false,
};
},
created() {
this.isOffline = !navigator.onLine;
window.addEventListener('offline', this.onOffline);
window.addEventListener('online', this.onOnline);
},
destroyed() {
window.removeEventListener('offline', this.onOffline);
window.removeEventListener('online', this.onOnline);
},
methods: {
onOffline() {
this.isOffline = true;
},
onOnline() {
this.isOffline = false;
},
},
};
27 changes: 11 additions & 16 deletions kolibri_explore_plugin/assets/src/views/welcome/PackReadyPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<b-button
pill
class="mb-4 mt-4"
:disabled="isOffline"
:disabled="isDownloadRequired && isOffline"
variant="primary"
@click="downloadContent"
>
Expand All @@ -28,40 +28,35 @@
<script>

import { PageNames } from '../../constants';
import isOfflineMixin from '../../mixins/isOfflineMixin';
import { getCollectionInfo } from '../../modules/coreExplore/utils';
import WelcomeBase from './WelcomeBase';

export default {
name: 'PackReadyPage',
components: {
WelcomeBase,
},
mixins: [isOfflineMixin],
data() {
return {
isOffline: false,
PageNames,
isDownloadRequired: true,
};
},
created() {
this.isOffline = !navigator.onLine;
window.addEventListener('offline', this.onOffline);
window.addEventListener('online', this.onOnline);
},
destroyed() {
window.removeEventListener('offline', this.onOffline);
window.removeEventListener('online', this.onOnline);
mounted() {
return getCollectionInfo(this.$route.params.grade, this.$route.params.name).then(
collectionsInfo => {
this.isDownloadRequired = collectionsInfo.isDownloadRequired;
}
);
},
methods: {
downloadContent() {
const grade = this.$route.params.grade;
const name = this.$route.params.name;
this.$router.push({ name: PageNames.DOWNLOAD, params: { grade, name } });
},
onOffline() {
this.isOffline = true;
},
onOnline() {
this.isOffline = false;
},
},
$trs: {
packReadyTitle: {
Expand Down
74 changes: 31 additions & 43 deletions kolibri_explore_plugin/collectionviews.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2022-2023 Endless OS Foundation LLC
# SPDX-License-Identifier: GPL-2.0-or-later
import itertools
import logging
import os
import time
Expand Down Expand Up @@ -147,12 +148,20 @@ def set_availability(self, free_space_gb):
def get_channels_count(self):
return len(self.get_channel_ids())

def get_channelimport_tasks(self):
def is_download_required(self):
return any(
itertools.chain(
self.iter_channelimport_tasks(),
self.iter_contentimport_tasks(),
self.iter_contentthumbnail_tasks(),
)
)

def iter_channelimport_tasks(self):
"""Return a serializable object to create channelimport tasks

For all the channels in this content manifest.
"""
tasks = []
for channel_id, channel_version in self.get_latest_channels():
metadata = get_channel_metadata(channel_id)
if metadata and metadata.version >= channel_version:
Expand All @@ -161,16 +170,14 @@ def get_channelimport_tasks(self):
"already present"
)
continue
tasks.append(get_remotechannelimport_task(channel_id))
return tasks
yield get_remotechannelimport_task(channel_id)

def get_extra_channelimport_tasks(self):
def iter_extra_channelimport_tasks(self):
"""Return a serializable object to create extra channelimport tasks

For all channels featured in Endless Key content manifests. In addition
to the channel metadata, all thumbnails are downloaded.
"""
tasks = []
for channel_id, channel_version in self.get_latest_extra_channels():
# Check if the channel metadata and thumbnails are already
# available.
Expand All @@ -192,21 +199,15 @@ def get_extra_channelimport_tasks(self):
)
continue

tasks.append(
get_remoteimport_task(
channel_id, node_ids=[], all_thumbnails=True
)
yield get_remoteimport_task(
channel_id, node_ids=[], all_thumbnails=True
)

return tasks

def get_contentimport_tasks(self):
def iter_contentimport_tasks(self):
"""Return a serializable object to create contentimport tasks

For all the channels in this content manifest.
"""
tasks = []

for channel_id in self.get_channel_ids():
channel_metadata = get_channel_metadata(channel_id)
node_ids = list(
Expand All @@ -226,38 +227,28 @@ def get_contentimport_tasks(self):
)
continue

tasks.append(
get_remotecontentimport_task(
channel_id, channel_metadata.name, node_ids
)
yield get_remotecontentimport_task(
channel_id, channel_metadata.name, node_ids
)

return tasks

def get_applyexternaltags_tasks(self):
def iter_applyexternaltags_tasks(self):
"""Return a serializable object to create applyexternaltags tasks

As defined in this content manifest metadata.
"""
if "tagged_node_ids" not in self.metadata:
return []

tasks = []

for tagged in self.metadata["tagged_node_ids"]:
node_id = tagged["node_id"]
tags = tagged["tags"]
tasks.append(get_applyexternaltags_task(node_id, tags))

return tasks
yield get_applyexternaltags_task(node_id, tags)

def get_contentthumbnail_tasks(self):
def iter_contentthumbnail_tasks(self):
"""Return a serializable object to create thumbnail contentimport tasks

For all the channels in this content manifest.
"""
tasks = []

for channel_id in self.get_channel_ids():
# Check if the desired thumbnail nodes are already available.
num_resources, _, _ = get_import_export_data(
Expand All @@ -273,14 +264,10 @@ def get_contentthumbnail_tasks(self):
)
continue

tasks.append(
get_remotecontentimport_task(
channel_id, node_ids=[], all_thumbnails=True
)
yield get_remotecontentimport_task(
channel_id, node_ids=[], all_thumbnails=True
)

return tasks

def _get_node_ids_for_channel(self, channel_metadata, channel_id):
"""Get node IDs regardless of the version

Expand Down Expand Up @@ -573,29 +560,29 @@ def _set_next_stage(self, user):
while not tasks and self._stage != DownloadStage.COMPLETED:
self._stage = DownloadStage(self._stage + 1)
if self._stage == DownloadStage.IMPORTING_CHANNELS:
tasks = self._content_manifest.get_channelimport_tasks()
tasks = self._content_manifest.iter_channelimport_tasks()
elif self._stage == DownloadStage.IMPORTING_CONTENT:
tasks = self._content_manifest.get_contentimport_tasks()
tasks = self._content_manifest.iter_contentimport_tasks()
elif self._stage == DownloadStage.APPLYING_EXTERNAL_TAGS:
tasks = self._content_manifest.get_applyexternaltags_tasks()
tasks = self._content_manifest.iter_applyexternaltags_tasks()

if self._stage == DownloadStage.COMPLETED:
logger.info("Download completed!")

# Download the manifest content thumbnails and the extra channels
# in the background.
thumbnail_tasks = (
self._content_manifest.get_contentthumbnail_tasks()
self._content_manifest.iter_contentthumbnail_tasks()
)
extra_channel_tasks = (
self._content_manifest.get_extra_channelimport_tasks()
self._content_manifest.iter_extra_channelimport_tasks()
)
for task in thumbnail_tasks + extra_channel_tasks:
for task in itertools.chain(thumbnail_tasks, extra_channel_tasks):
BackgroundTask.create_from_task_data(task)
logger.info("Starting background download tasks")
enqueue_next_background_task()

self._tasks_pending = tasks
self._tasks_pending = list(tasks)
self._tasks_previously_completed.extend(self._tasks_completed)
self._tasks_completed = []
logger.info(f"Started download stage: {self._stage.name}")
Expand Down Expand Up @@ -685,6 +672,7 @@ def _get_collections_info_by_grade_name(grade, name):
"metadata": manifest.metadata,
"available": manifest.available,
"channelsCount": manifest.get_channels_count(),
"isDownloadRequired": manifest.is_download_required(),
}


Expand Down
2 changes: 2 additions & 0 deletions kolibri_explore_plugin/test/test_collectionviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def test_get_collection_info():
"metadata": collection_data["metadata"],
"available": True,
"channelsCount": len(collection_data["channels"]),
"isDownloadRequired": True,
}
}

Expand Down Expand Up @@ -81,6 +82,7 @@ def test_get_all_collections_info():
"metadata": collection_data["metadata"],
"available": True,
"channelsCount": len(collection_data["channels"]),
"isDownloadRequired": True,
}
)
expected_data = {"allCollectionsInfo": all_collections_info}
Expand Down