feat(media): delete videos from the media library
Add an admin flow to permanently delete a media asset and clean up everything it leaves behind. - Delete dialog with a destructive, irreversible confirmation; deletion is blocked while the video is still attached and shows the in-use lessons so the admin can detach first. - Usage list resolves lesson/course context for each attachment, links to the lesson editor via an anchor (right-click "open in new tab"), and exposes a refresh button to re-check usage after detaching elsewhere. - Storage cleanup runs as a background maintenance job: the producer captures the asset's object keys/prefixes up front and an idempotent worker sweeps HLS segments, thumbnails, audio, transcript, and the raw upload from object storage. Also harden usage accounting so stale attachments cannot trap an asset: - asset_usages has no FK to its polymorphic target, so prune a lesson's usage rows whenever the lesson is hard-deleted (single + bulk paths). - Usage resolution drops orphaned lesson usages (lesson gone or course soft-deleted) so they neither appear in the list nor block deletion. Document the icon-only button convention (variant="secondary") in AGENTS.md.
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
AssetUsage,
|
||||
AssetUsageGraph,
|
||||
CreateAssetRequest,
|
||||
DeleteAssetRequest,
|
||||
GetAssetTranscriptRequest,
|
||||
GetAssetUsageRequest,
|
||||
GetYouTubeMetadataRequest,
|
||||
@@ -273,6 +274,30 @@ export class MediaApi extends BaseApiWithErrors {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete an asset and its stored files. The API blocks deletion
|
||||
* while the asset is still attached anywhere (409 ASSET_IN_USE). Returns true
|
||||
* only when the asset was actually removed.
|
||||
*/
|
||||
async deleteAsset(assetId: string): Promise<boolean> {
|
||||
let deleted = false;
|
||||
await this.execute<DeleteAssetRequest>({
|
||||
requestFn: () => classroomio.organization.assets[':assetId'].$delete({ param: { assetId } }),
|
||||
logContext: 'deleting media asset',
|
||||
onSuccess: () => {
|
||||
deleted = true;
|
||||
this.assets = this.assets.filter((asset) => asset.id !== assetId);
|
||||
snackbar.success('snackbar.media_manager.delete_success');
|
||||
},
|
||||
onError: (error) => {
|
||||
const inUse = typeof error === 'object' && error !== null && 'code' in error && error.code === 'ASSET_IN_USE';
|
||||
snackbar.error(inUse ? 'snackbar.media_manager.delete_in_use' : 'snackbar.media_manager.delete_failed');
|
||||
}
|
||||
});
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
async getVideoPlaybackUrl(asset: OrganizationAsset): Promise<string | null> {
|
||||
return this.getAssetDownloadUrl(asset, { forPlayback: true });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import ImageIcon from '@lucide/svelte/icons/image';
|
||||
import LinkIcon from '@lucide/svelte/icons/link';
|
||||
import PencilIcon from '@lucide/svelte/icons/pencil';
|
||||
import Trash2Icon from '@lucide/svelte/icons/trash-2';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
@@ -27,6 +28,7 @@
|
||||
onUsage?: (asset: OrganizationAsset) => void;
|
||||
onDownload?: (asset: OrganizationAsset) => void;
|
||||
onManageThumbnails?: (asset: OrganizationAsset) => void;
|
||||
onDelete?: (asset: OrganizationAsset) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -35,7 +37,8 @@
|
||||
onEdit = () => {},
|
||||
onUsage = () => {},
|
||||
onDownload = () => {},
|
||||
onManageThumbnails = () => {}
|
||||
onManageThumbnails = () => {},
|
||||
onDelete = () => {}
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
@@ -87,6 +90,13 @@
|
||||
{$t('media_manager.actions.download')}
|
||||
</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item variant="destructive" onclick={() => onDelete(asset)}>
|
||||
<span class="flex items-center gap-2">
|
||||
<Trash2Icon size={14} />
|
||||
{$t('media_manager.actions.delete')}
|
||||
</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import * as Tooltip from '@cio/ui/base/tooltip';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
|
||||
import AssetUsageList from './asset-usage-list.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
formatBytes,
|
||||
formatUsageDate,
|
||||
getAssetDisplayName,
|
||||
getKindIcon,
|
||||
getTargetTypeLabel,
|
||||
type AssetUsageGraph,
|
||||
type OrganizationAsset
|
||||
} from '$features/media/utils';
|
||||
@@ -19,6 +24,7 @@
|
||||
selectedAsset?: OrganizationAsset | null;
|
||||
usageData?: AssetUsageGraph | null;
|
||||
isLoading?: boolean;
|
||||
onRefresh?: () => void;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -27,6 +33,7 @@
|
||||
selectedAsset = null,
|
||||
usageData = null,
|
||||
isLoading = false,
|
||||
onRefresh = () => {},
|
||||
onOpenChange = () => {}
|
||||
}: Props = $props();
|
||||
|
||||
@@ -96,42 +103,37 @@
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<p class="ui:text-muted-foreground text-sm">{$t('media_manager.usage.loading')}</p>
|
||||
<Empty description={$t('media_manager.usage.loading')} icon={Spinner} class="ui:max-h-40 py-6" />
|
||||
{:else if isUsageEmpty}
|
||||
<p class="ui:text-muted-foreground text-sm">{$t('media_manager.usage.empty')}</p>
|
||||
{:else if usageData}
|
||||
<p class="ui:text-muted-foreground mb-3 text-sm">
|
||||
{$t('media_manager.usage.total')}
|
||||
{usageData.usageCount}
|
||||
</p>
|
||||
<div class="max-h-[420px] overflow-auto rounded-lg border">
|
||||
<div class="divide-y">
|
||||
{#each usageData.usages as usage (usage.id)}
|
||||
<div class="grid grid-cols-1 gap-2 p-3 text-sm md:grid-cols-5 md:items-center md:gap-4">
|
||||
<div>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('media_manager.usage.table.target_type')}</p>
|
||||
<p>{getTargetTypeLabel(usage.targetType)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('media_manager.usage.table.target_id')}</p>
|
||||
<p class="font-mono text-xs">{usage.targetId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('media_manager.usage.table.slot')}</p>
|
||||
<p>{usage.slotType}{usage.slotKey ? ` (${usage.slotKey})` : ''}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('media_manager.usage.table.position')}</p>
|
||||
<p>{usage.position ?? t.get('media_manager.common.not_available')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="ui:text-muted-foreground text-xs">{$t('media_manager.usage.table.added')}</p>
|
||||
<p>{formatUsageDate(usage.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mb-3 flex items-center justify-between gap-2">
|
||||
<p class="ui:text-muted-foreground text-sm">
|
||||
{$t('media_manager.usage.total')}
|
||||
{usageData.usageCount}
|
||||
</p>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onclick={() => onRefresh()}
|
||||
aria-label={$t('media_manager.usage.refresh')}
|
||||
>
|
||||
<RefreshCwIcon size={16} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="top" sideOffset={4}>
|
||||
{$t('media_manager.usage.refresh')}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
<AssetUsageList usages={usageData.usages} onNavigate={() => handleOpenChange(false)} />
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
||||
|
||||
import { resolve } from '$app/paths';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { formatUsageDate, getSlotTypeLabel, getTargetTypeLabel, type AssetUsageGraph } from '$features/media/utils';
|
||||
|
||||
interface Props {
|
||||
usages: AssetUsageGraph['usages'];
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
let { usages, onNavigate = () => {} }: Props = $props();
|
||||
|
||||
function lessonHref(courseId: string, lessonId: string) {
|
||||
return resolve(`/courses/${courseId}/lessons/${lessonId}`, {});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-h-[420px] overflow-auto rounded-lg border">
|
||||
<div class="divide-y">
|
||||
{#each usages as usage (usage.id)}
|
||||
<div class="flex flex-col gap-2 p-3 text-sm md:flex-row md:items-center md:justify-between md:gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate font-medium">
|
||||
{usage.targetTitle ?? getTargetTypeLabel(usage.targetType)}
|
||||
</p>
|
||||
<p class="ui:text-muted-foreground mt-0.5 truncate text-xs">
|
||||
{#if usage.courseTitle}
|
||||
{usage.courseTitle} ·
|
||||
{/if}
|
||||
{getSlotTypeLabel(usage.slotType)}{usage.slotKey ? ` (${usage.slotKey})` : ''} ·
|
||||
{$t('media_manager.usage.table.added')}
|
||||
{formatUsageDate(usage.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if usage.targetType === 'lesson' && usage.courseId}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
href={lessonHref(usage.courseId!, usage.targetId)}
|
||||
onclick={() => onNavigate()}
|
||||
>
|
||||
<ExternalLinkIcon size={14} class="mr-1.5" />
|
||||
{$t('media_manager.usage.go_to_lesson')}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import * as Alert from '@cio/ui/base/alert';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import * as Tooltip from '@cio/ui/base/tooltip';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
|
||||
|
||||
import AssetUsageList from './asset-usage-list.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { getAssetDisplayName, type AssetUsageGraph, type OrganizationAsset } from '$features/media/utils';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
asset?: OrganizationAsset | null;
|
||||
usageData?: AssetUsageGraph | null;
|
||||
isLoadingUsage?: boolean;
|
||||
isDeleting?: boolean;
|
||||
onConfirm?: () => void;
|
||||
onRefresh?: () => void;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
asset = null,
|
||||
usageData = null,
|
||||
isLoadingUsage = false,
|
||||
isDeleting = false,
|
||||
onConfirm = () => {},
|
||||
onRefresh = () => {},
|
||||
onOpenChange = () => {}
|
||||
}: Props = $props();
|
||||
|
||||
const isInUse = $derived((usageData?.usageCount ?? 0) > 0);
|
||||
|
||||
function handleOpenChange(isOpen: boolean) {
|
||||
open = isOpen;
|
||||
if (!isOpen) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open onOpenChange={(isOpen) => handleOpenChange(isOpen)}>
|
||||
<Dialog.Content class="w-[95%] max-w-2xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('media_manager.delete.title')}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{#if asset}
|
||||
<span class="font-medium">{getAssetDisplayName(asset)}</span>
|
||||
{/if}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if isLoadingUsage}
|
||||
<Empty description={$t('media_manager.usage.loading')} icon={Spinner} class="ui:max-h-40 py-6" />
|
||||
{:else if isInUse}
|
||||
<Alert.Root variant="warning">
|
||||
<TriangleAlertIcon />
|
||||
<Alert.Title>{$t('media_manager.delete.in_use_title')}</Alert.Title>
|
||||
<Alert.Description>{$t('media_manager.delete.in_use_description')}</Alert.Description>
|
||||
</Alert.Root>
|
||||
|
||||
<div class="mt-2 mb-1 flex items-center justify-between gap-2">
|
||||
<p class="ui:text-muted-foreground text-sm">
|
||||
{$t('media_manager.usage.total')}
|
||||
{usageData?.usageCount}
|
||||
</p>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onclick={() => onRefresh()}
|
||||
aria-label={$t('media_manager.usage.refresh')}
|
||||
>
|
||||
<RefreshCwIcon size={16} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="top" sideOffset={4}>
|
||||
{$t('media_manager.usage.refresh')}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
<AssetUsageList usages={usageData?.usages ?? []} onNavigate={() => handleOpenChange(false)} />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => handleOpenChange(false)}>
|
||||
{$t('media_manager.delete.close')}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
{:else}
|
||||
<Alert.Root variant="destructive">
|
||||
<TriangleAlertIcon />
|
||||
<Alert.Title>{$t('media_manager.delete.irreversible_title')}</Alert.Title>
|
||||
<Alert.Description>{$t('media_manager.delete.irreversible_description')}</Alert.Description>
|
||||
</Alert.Root>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" disabled={isDeleting} onclick={() => handleOpenChange(false)}>
|
||||
{$t('media_manager.delete.cancel')}
|
||||
</Button>
|
||||
<Button variant="destructive" disabled={isDeleting} onclick={onConfirm}>
|
||||
{$t('media_manager.delete.confirm')}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,5 +1,7 @@
|
||||
export { default as AssetCard } from './asset-card.svelte';
|
||||
export { default as AssetUsageDialog } from './asset-usage-dialog.svelte';
|
||||
export { default as AssetUsageList } from './asset-usage-list.svelte';
|
||||
export { default as DeleteAssetDialog } from './delete-asset-dialog.svelte';
|
||||
export { default as EditAssetDialog } from './edit-asset-dialog.svelte';
|
||||
export { default as ManageThumbnailsDialog } from './manage-thumbnails-dialog.svelte';
|
||||
export { default as MediaFilters } from './media-filters.svelte';
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import {
|
||||
AssetCard,
|
||||
AssetUsageDialog,
|
||||
DeleteAssetDialog,
|
||||
EditAssetDialog,
|
||||
ManageThumbnailsDialog,
|
||||
MediaFilters,
|
||||
@@ -36,8 +37,10 @@
|
||||
let editOpen = $state(false);
|
||||
let usageOpen = $state(false);
|
||||
let manageThumbsOpen = $state(false);
|
||||
let deleteOpen = $state(false);
|
||||
let isSavingAsset = $state(false);
|
||||
let isUsageLoading = $state(false);
|
||||
let isDeleting = $state(false);
|
||||
let downloadingAssetId = $state<string | null>(null);
|
||||
let selectedAsset = $state<OrganizationAsset | null>(null);
|
||||
let usageData = $state<AssetUsageGraph | null>(null);
|
||||
@@ -96,11 +99,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function openUsage(asset: OrganizationAsset) {
|
||||
selectedAsset = asset;
|
||||
usageOpen = true;
|
||||
async function loadUsage(asset: OrganizationAsset) {
|
||||
isUsageLoading = true;
|
||||
usageData = null;
|
||||
try {
|
||||
usageData = await mediaApi.getAssetUsage(asset.id);
|
||||
} finally {
|
||||
@@ -108,12 +108,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function openUsage(asset: OrganizationAsset) {
|
||||
selectedAsset = asset;
|
||||
usageOpen = true;
|
||||
usageData = null;
|
||||
await loadUsage(asset);
|
||||
}
|
||||
|
||||
async function refreshUsage() {
|
||||
if (!selectedAsset) return;
|
||||
|
||||
await loadUsage(selectedAsset);
|
||||
}
|
||||
|
||||
function resetUsageModalState() {
|
||||
selectedAsset = null;
|
||||
usageData = null;
|
||||
isUsageLoading = false;
|
||||
}
|
||||
|
||||
async function openDelete(asset: OrganizationAsset) {
|
||||
selectedAsset = asset;
|
||||
deleteOpen = true;
|
||||
usageData = null;
|
||||
await loadUsage(asset);
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!selectedAsset) return;
|
||||
|
||||
isDeleting = true;
|
||||
try {
|
||||
const deleted = await mediaApi.deleteAsset(selectedAsset.id);
|
||||
if (!deleted) return;
|
||||
|
||||
deleteOpen = false;
|
||||
await refreshStorageSummary();
|
||||
} finally {
|
||||
isDeleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetUsageModalState();
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAsset(asset: OrganizationAsset) {
|
||||
downloadingAssetId = asset.id;
|
||||
try {
|
||||
@@ -168,6 +209,7 @@
|
||||
onUsage={openUsage}
|
||||
onDownload={downloadAsset}
|
||||
onManageThumbnails={openManageThumbnails}
|
||||
onDelete={openDelete}
|
||||
/>
|
||||
{/each}
|
||||
</Item.Group>
|
||||
@@ -208,5 +250,17 @@
|
||||
{selectedAsset}
|
||||
{usageData}
|
||||
isLoading={isUsageLoading}
|
||||
onRefresh={refreshUsage}
|
||||
onOpenChange={handleUsageOpenChange}
|
||||
/>
|
||||
|
||||
<DeleteAssetDialog
|
||||
bind:open={deleteOpen}
|
||||
asset={selectedAsset}
|
||||
{usageData}
|
||||
isLoadingUsage={isUsageLoading}
|
||||
{isDeleting}
|
||||
onConfirm={confirmDelete}
|
||||
onRefresh={refreshUsage}
|
||||
onOpenChange={handleDeleteOpenChange}
|
||||
/>
|
||||
|
||||
@@ -110,6 +110,14 @@ export function getTargetTypeLabel(targetType: string): string {
|
||||
return t.get(keyMap[targetType] ?? 'media_manager.usage.target.unknown');
|
||||
}
|
||||
|
||||
export function getSlotTypeLabel(slotType: string): string {
|
||||
const keyMap: Record<string, string> = {
|
||||
lesson_video: 'media_manager.usage.slot_type.lesson_video',
|
||||
lesson_document: 'media_manager.usage.slot_type.lesson_document'
|
||||
};
|
||||
return t.get(keyMap[slotType] ?? 'media_manager.usage.slot_type.unknown');
|
||||
}
|
||||
|
||||
export function formatUsageDate(value: string | null | undefined): string {
|
||||
if (!value) {
|
||||
return t.get('media_manager.common.not_available');
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "Kunne ikke genskabe miniaturebilleder",
|
||||
"transcript_saved": "Udskrift opdateret",
|
||||
"transcript_save_failed": "Kunne ikke gemme transskription",
|
||||
"transcription_already_running": "Et transskriptionsjob er allerede i gang for denne video"
|
||||
"transcription_already_running": "Et transskriptionsjob er allerede i gang for denne video",
|
||||
"video_added_to_lesson": "Video tilføjet til lektion",
|
||||
"delete_success": "Video slettet",
|
||||
"delete_in_use": "Denne video er stadig knyttet til en lektion. Fjern det der først.",
|
||||
"delete_failed": "Videoen kunne ikke slettes"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Taggruppe oprettet",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "Motion",
|
||||
"question": "Spørgsmål",
|
||||
"unknown": "Ukendt"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "Gå til lektion",
|
||||
"slot_type": {
|
||||
"lesson_video": "Lektion video",
|
||||
"lesson_document": "Lektionsdokument",
|
||||
"unknown": "Vedhæftet fil"
|
||||
},
|
||||
"refresh": "Opdater brugen"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Filnavn",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Rediger",
|
||||
"where_used": "Hvor brugt",
|
||||
"download": "Download",
|
||||
"manage_thumbnails": "Administrer thumbnails"
|
||||
"manage_thumbnails": "Administrer thumbnails",
|
||||
"delete": "Slet"
|
||||
},
|
||||
"table": {
|
||||
"title": "Titel",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Forhåndsvisning af tilpasset miniaturebillede",
|
||||
"regenerate": "Regenerer",
|
||||
"save_selection": "Gem thumbnail"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Slet video",
|
||||
"irreversible_title": "Dette kan ikke fortrydes",
|
||||
"irreversible_description": "Denne video og alle dens filer – gengivelser, miniaturebilleder og billedtekster – slettes permanent og kan ikke gendannes.",
|
||||
"in_use_title": "Stadig i brug",
|
||||
"in_use_description": "Denne video er vedhæftet nedenstående lektioner. Åbn hver lektion, fjern videoen der, og kom så tilbage for at slette den.",
|
||||
"confirm": "Slet permanent",
|
||||
"cancel": "Annuller",
|
||||
"close": "Luk"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "Die Miniaturansichten konnten nicht neu generiert werden",
|
||||
"transcript_saved": "Transkript aktualisiert",
|
||||
"transcript_save_failed": "Das Transkript konnte nicht gespeichert werden",
|
||||
"transcription_already_running": "Für dieses Video wird bereits ein Transkriptionsauftrag ausgeführt"
|
||||
"transcription_already_running": "Für dieses Video wird bereits ein Transkriptionsauftrag ausgeführt",
|
||||
"video_added_to_lesson": "Video zur Lektion hinzugefügt",
|
||||
"delete_success": "Video gelöscht",
|
||||
"delete_in_use": "Dieses Video ist noch einer Lektion beigefügt. Entfernen Sie es zuerst dort.",
|
||||
"delete_failed": "Video konnte nicht gelöscht werden"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Tag-Gruppe erstellt",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "Übung",
|
||||
"question": "Frage",
|
||||
"unknown": "Unbekannt"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "Gehe zur Lektion",
|
||||
"slot_type": {
|
||||
"lesson_video": "Unterrichtsvideo",
|
||||
"lesson_document": "Unterrichtsdokument",
|
||||
"unknown": "Anhang"
|
||||
},
|
||||
"refresh": "Nutzung aktualisieren"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Dateiname",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Bearbeiten",
|
||||
"where_used": "Wo verwendet",
|
||||
"download": "Herunterladen",
|
||||
"manage_thumbnails": "Miniaturansichten verwalten"
|
||||
"manage_thumbnails": "Miniaturansichten verwalten",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"table": {
|
||||
"title": "Titel",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Benutzerdefinierte Miniaturvorschau",
|
||||
"regenerate": "Regenerieren",
|
||||
"save_selection": "Miniaturansicht speichern"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Video löschen",
|
||||
"irreversible_title": "Dies kann nicht rückgängig gemacht werden",
|
||||
"irreversible_description": "Dieses Video und alle seine Dateien – Wiedergaben, Miniaturansichten und Untertitel – werden dauerhaft gelöscht und können nicht wiederhergestellt werden.",
|
||||
"in_use_title": "Immer noch im Einsatz",
|
||||
"in_use_description": "Dieses Video ist den Lektionen unten beigefügt. Öffnen Sie jede Lektion, entfernen Sie das Video dort und kehren Sie dann zurück, um es zu löschen.",
|
||||
"confirm": "Dauerhaft löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"close": "Schließen"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -1081,7 +1081,18 @@
|
||||
"edit": "Edit",
|
||||
"where_used": "Where used",
|
||||
"download": "Download",
|
||||
"manage_thumbnails": "Manage thumbnails"
|
||||
"manage_thumbnails": "Manage thumbnails",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete video",
|
||||
"irreversible_title": "This cannot be undone",
|
||||
"irreversible_description": "This video and all of its files — renditions, thumbnails, and captions — will be permanently deleted and cannot be recovered.",
|
||||
"in_use_title": "Still in use",
|
||||
"in_use_description": "This video is attached to the lessons below. Open each lesson, remove the video there, then come back to delete it.",
|
||||
"confirm": "Delete permanently",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close"
|
||||
},
|
||||
"pagination": {
|
||||
"previous": "Previous",
|
||||
@@ -1128,12 +1139,19 @@
|
||||
"loading": "Loading usage details...",
|
||||
"empty": "This asset is not attached anywhere yet.",
|
||||
"total": "Total usages:",
|
||||
"go_to_lesson": "Go to lesson",
|
||||
"refresh": "Refresh usage",
|
||||
"target": {
|
||||
"lesson": "Lesson",
|
||||
"exercise": "Exercise",
|
||||
"question": "Question",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"slot_type": {
|
||||
"lesson_video": "Lesson video",
|
||||
"lesson_document": "Lesson document",
|
||||
"unknown": "Attachment"
|
||||
},
|
||||
"table": {
|
||||
"target_type": "Target Type",
|
||||
"target_id": "Target ID",
|
||||
@@ -4121,6 +4139,9 @@
|
||||
"update_failed": "Failed to update media asset",
|
||||
"update_success": "Media asset updated",
|
||||
"usage_failed": "Failed to load asset usage",
|
||||
"delete_success": "Video deleted",
|
||||
"delete_in_use": "This video is still attached to a lesson. Remove it there first.",
|
||||
"delete_failed": "Failed to delete video",
|
||||
"download_failed": "Failed to get asset download URL",
|
||||
"thumbnail_uploaded": "Thumbnail uploaded",
|
||||
"thumbnail_upload_failed": "Failed to upload thumbnail",
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "No se pudieron regenerar las miniaturas",
|
||||
"transcript_saved": "Transcripción actualizada",
|
||||
"transcript_save_failed": "No se pudo guardar la transcripción",
|
||||
"transcription_already_running": "Ya se está realizando un trabajo de transcripción para este vídeo."
|
||||
"transcription_already_running": "Ya se está realizando un trabajo de transcripción para este vídeo.",
|
||||
"video_added_to_lesson": "Vídeo añadido a la lección.",
|
||||
"delete_success": "Vídeo eliminado",
|
||||
"delete_in_use": "Este vídeo todavía está adjunto a una lección. Quítelo allí primero.",
|
||||
"delete_failed": "No se pudo eliminar el video"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Grupo de etiquetas creado",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "Ejercicio",
|
||||
"question": "Pregunta",
|
||||
"unknown": "Desconocido"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "ir a la lección",
|
||||
"slot_type": {
|
||||
"lesson_video": "Vídeo de la lección",
|
||||
"lesson_document": "documento de lección",
|
||||
"unknown": "Adjunto"
|
||||
},
|
||||
"refresh": "Actualizar uso"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Nombre de archivo",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Editar",
|
||||
"where_used": "donde se usa",
|
||||
"download": "Descargar",
|
||||
"manage_thumbnails": "Administrar miniaturas"
|
||||
"manage_thumbnails": "Administrar miniaturas",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"table": {
|
||||
"title": "Título",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Vista previa en miniatura personalizada",
|
||||
"regenerate": "regenerar",
|
||||
"save_selection": "Guardar miniatura"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Eliminar vídeo",
|
||||
"irreversible_title": "Esto no se puede deshacer",
|
||||
"irreversible_description": "Este video y todos sus archivos (representaciones, miniaturas y subtítulos) se eliminarán permanentemente y no se podrán recuperar.",
|
||||
"in_use_title": "Todavía en uso",
|
||||
"in_use_description": "Este vídeo se adjunta a las lecciones siguientes. Abra cada lección, elimine el video allí y luego regrese para eliminarlo.",
|
||||
"confirm": "Eliminar permanentemente",
|
||||
"cancel": "Cancelar",
|
||||
"close": "Cerrar"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "Échec de la régénération des miniatures",
|
||||
"transcript_saved": "Transcription mise à jour",
|
||||
"transcript_save_failed": "Impossible d'enregistrer la transcription",
|
||||
"transcription_already_running": "Un travail de transcription est déjà en cours pour cette vidéo"
|
||||
"transcription_already_running": "Un travail de transcription est déjà en cours pour cette vidéo",
|
||||
"video_added_to_lesson": "Vidéo ajoutée à la leçon",
|
||||
"delete_success": "Vidéo supprimée",
|
||||
"delete_in_use": "Cette vidéo est toujours jointe à une leçon. Retirez-le d'abord.",
|
||||
"delete_failed": "Échec de la suppression de la vidéo"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Groupe de balises créé",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "Exercice",
|
||||
"question": "Question",
|
||||
"unknown": "Inconnu"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "Aller à la leçon",
|
||||
"slot_type": {
|
||||
"lesson_video": "Vidéo de cours",
|
||||
"lesson_document": "Document de cours",
|
||||
"unknown": "Pièce jointe"
|
||||
},
|
||||
"refresh": "Actualiser l'utilisation"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Nom du fichier",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Modifier",
|
||||
"where_used": "Où utilisé",
|
||||
"download": "Télécharger",
|
||||
"manage_thumbnails": "Gérer les vignettes"
|
||||
"manage_thumbnails": "Gérer les vignettes",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"table": {
|
||||
"title": "Titre",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Aperçu miniature personnalisé",
|
||||
"regenerate": "Régénérer",
|
||||
"save_selection": "Enregistrer la vignette"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Supprimer la vidéo",
|
||||
"irreversible_title": "Cela ne peut pas être annulé",
|
||||
"irreversible_description": "Cette vidéo et tous ses fichiers (rendus, miniatures et sous-titres) seront définitivement supprimés et ne pourront pas être récupérés.",
|
||||
"in_use_title": "Toujours utilisé",
|
||||
"in_use_description": "Cette vidéo est jointe aux leçons ci-dessous. Ouvrez chaque leçon, supprimez-y la vidéo, puis revenez la supprimer.",
|
||||
"confirm": "Supprimer définitivement",
|
||||
"cancel": "Annuler",
|
||||
"close": "Fermer"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "थंबनेल पुन: उत्पन्न करने में विफल",
|
||||
"transcript_saved": "प्रतिलेख अद्यतन किया गया",
|
||||
"transcript_save_failed": "प्रतिलेख सहेजा नहीं जा सका",
|
||||
"transcription_already_running": "इस वीडियो के लिए प्रतिलेखन कार्य पहले से ही प्रगति पर है"
|
||||
"transcription_already_running": "इस वीडियो के लिए प्रतिलेखन कार्य पहले से ही प्रगति पर है",
|
||||
"video_added_to_lesson": "पाठ में वीडियो जोड़ा गया",
|
||||
"delete_success": "वीडियो हटा दिया गया",
|
||||
"delete_in_use": "यह वीडियो अभी भी एक पाठ से जुड़ा हुआ है। पहले इसे वहां से हटाएं.",
|
||||
"delete_failed": "वीडियो हटाने में विफल"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "टैग समूह बनाया गया",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "व्यायाम",
|
||||
"question": "प्रश्न",
|
||||
"unknown": "अज्ञात"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "पाठ पर जाएँ",
|
||||
"slot_type": {
|
||||
"lesson_video": "पाठ वीडियो",
|
||||
"lesson_document": "पाठ दस्तावेज़",
|
||||
"unknown": "लगाव"
|
||||
},
|
||||
"refresh": "उपयोग ताज़ा करें"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "फ़ाइल का नाम",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "संपादित करें",
|
||||
"where_used": "कहां उपयोग किया जाता है",
|
||||
"download": "डाउनलोड करें",
|
||||
"manage_thumbnails": "थंबनेल प्रबंधित करें"
|
||||
"manage_thumbnails": "थंबनेल प्रबंधित करें",
|
||||
"delete": "हटाएँ"
|
||||
},
|
||||
"table": {
|
||||
"title": "शीर्षक",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "कस्टम थंबनेल पूर्वावलोकन",
|
||||
"regenerate": "पुनर्जीवित करें",
|
||||
"save_selection": "थंबनेल सहेजें"
|
||||
},
|
||||
"delete": {
|
||||
"title": "वीडियो हटाएँ",
|
||||
"irreversible_title": "इसे पूर्ववत नहीं किया जा सकता",
|
||||
"irreversible_description": "यह वीडियो और इसकी सभी फ़ाइलें - प्रतिपादन, थंबनेल और कैप्शन - स्थायी रूप से हटा दिए जाएंगे और पुनर्प्राप्त नहीं किए जा सकेंगे।",
|
||||
"in_use_title": "अभी भी उपयोग में है",
|
||||
"in_use_description": "यह वीडियो नीचे दिए गए पाठों से जुड़ा हुआ है। प्रत्येक पाठ खोलें, वहां से वीडियो हटाएं, फिर उसे हटाने के लिए वापस आएं।",
|
||||
"confirm": "स्थायी रूप से हटाएँ",
|
||||
"cancel": "रद्द करें",
|
||||
"close": "बंद करें"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "Nie udało się ponownie wygenerować miniatur",
|
||||
"transcript_saved": "Transkrypcja zaktualizowana",
|
||||
"transcript_save_failed": "Nie udało się zapisać transkrypcji",
|
||||
"transcription_already_running": "Zadanie transkrypcji tego filmu wideo jest już w toku"
|
||||
"transcription_already_running": "Zadanie transkrypcji tego filmu wideo jest już w toku",
|
||||
"video_added_to_lesson": "Film dodany do lekcji",
|
||||
"delete_success": "Film usunięty",
|
||||
"delete_in_use": "Ten film jest nadal dołączony do lekcji. Najpierw usuń go tam.",
|
||||
"delete_failed": "Nie udało się usunąć filmu"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Utworzono grupę tagów",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "Ćwicz",
|
||||
"question": "Pytanie",
|
||||
"unknown": "Nieznany"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "Idź na lekcję",
|
||||
"slot_type": {
|
||||
"lesson_video": "Film z lekcją",
|
||||
"lesson_document": "Dokument lekcji",
|
||||
"unknown": "Załącznik"
|
||||
},
|
||||
"refresh": "Odśwież użycie"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Nazwa pliku",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Edytuj",
|
||||
"where_used": "Gdzie używany",
|
||||
"download": "Pobierz",
|
||||
"manage_thumbnails": "Zarządzaj miniaturami"
|
||||
"manage_thumbnails": "Zarządzaj miniaturami",
|
||||
"delete": "Usuń"
|
||||
},
|
||||
"table": {
|
||||
"title": "Tytuł",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Niestandardowy podgląd miniatur",
|
||||
"regenerate": "Zregeneruj się",
|
||||
"save_selection": "Zapisz miniaturę"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Usuń wideo",
|
||||
"irreversible_title": "Tego nie można cofnąć",
|
||||
"irreversible_description": "Ten film wideo i wszystkie jego pliki — wersje, miniatury i podpisy — zostaną trwale usunięte i nie będzie można ich odzyskać.",
|
||||
"in_use_title": "Nadal w użyciu",
|
||||
"in_use_description": "Ten film jest dołączony do poniższych lekcji. Otwórz każdą lekcję, usuń stamtąd wideo, a następnie wróć, aby je usunąć.",
|
||||
"confirm": "Usuń trwale",
|
||||
"cancel": "Anuluj",
|
||||
"close": "Zamknij"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "Falha ao regenerar miniaturas",
|
||||
"transcript_saved": "Transcrição atualizada",
|
||||
"transcript_save_failed": "Não foi possível salvar a transcrição",
|
||||
"transcription_already_running": "Um trabalho de transcrição já está em andamento para este vídeo"
|
||||
"transcription_already_running": "Um trabalho de transcrição já está em andamento para este vídeo",
|
||||
"video_added_to_lesson": "Vídeo adicionado à aula",
|
||||
"delete_success": "Vídeo excluído",
|
||||
"delete_in_use": "Este vídeo ainda está anexado a uma aula. Remova-o primeiro.",
|
||||
"delete_failed": "Falha ao excluir vídeo"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Grupo de tags criado",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "Exercício",
|
||||
"question": "Pergunta",
|
||||
"unknown": "Desconhecido"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "Vá para a aula",
|
||||
"slot_type": {
|
||||
"lesson_video": "Vídeo da aula",
|
||||
"lesson_document": "Documento da lição",
|
||||
"unknown": "Anexo"
|
||||
},
|
||||
"refresh": "Atualizar uso"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Nome do arquivo",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Editar",
|
||||
"where_used": "Onde usado",
|
||||
"download": "Baixar",
|
||||
"manage_thumbnails": "Gerenciar miniaturas"
|
||||
"manage_thumbnails": "Gerenciar miniaturas",
|
||||
"delete": "Excluir"
|
||||
},
|
||||
"table": {
|
||||
"title": "Título",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Pré-visualização de miniaturas personalizadas",
|
||||
"regenerate": "Regenerar",
|
||||
"save_selection": "Salvar miniatura"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Excluir vídeo",
|
||||
"irreversible_title": "Isso não pode ser desfeito",
|
||||
"irreversible_description": "Este vídeo e todos os seus arquivos – representações, miniaturas e legendas – serão excluídos permanentemente e não poderão ser recuperados.",
|
||||
"in_use_title": "Ainda em uso",
|
||||
"in_use_description": "Este vídeo está anexado às aulas abaixo. Abra cada lição, remova o vídeo e volte para excluí-lo.",
|
||||
"confirm": "Excluir permanentemente",
|
||||
"cancel": "Cancelar",
|
||||
"close": "Fechar"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "Не удалось восстановить миниатюры.",
|
||||
"transcript_saved": "Стенограмма обновлена",
|
||||
"transcript_save_failed": "Не удалось сохранить расшифровку",
|
||||
"transcription_already_running": "Транскрипция этого видео уже выполняется."
|
||||
"transcription_already_running": "Транскрипция этого видео уже выполняется.",
|
||||
"video_added_to_lesson": "Видео добавлено в урок",
|
||||
"delete_success": "Видео удалено",
|
||||
"delete_in_use": "Это видео все еще прикреплено к уроку. Сначала уберите его оттуда.",
|
||||
"delete_failed": "Не удалось удалить видео."
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Группа тегов создана.",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "Упражнение",
|
||||
"question": "Вопрос",
|
||||
"unknown": "Неизвестно"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "Перейти на урок",
|
||||
"slot_type": {
|
||||
"lesson_video": "Видео урока",
|
||||
"lesson_document": "Документ урока",
|
||||
"unknown": "Приложение"
|
||||
},
|
||||
"refresh": "Обновить использование"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Имя файла",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Редактировать",
|
||||
"where_used": "Где используется",
|
||||
"download": "Скачать",
|
||||
"manage_thumbnails": "Управление миниатюрами"
|
||||
"manage_thumbnails": "Управление миниатюрами",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"table": {
|
||||
"title": "Название",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Пользовательский предварительный просмотр миниатюр",
|
||||
"regenerate": "Регенерировать",
|
||||
"save_selection": "Сохранить миниатюру"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Удалить видео",
|
||||
"irreversible_title": "Это нельзя отменить",
|
||||
"irreversible_description": "Это видео и все его файлы — версии, миниатюры и подписи — будут удалены без возможности восстановления без возможности восстановления.",
|
||||
"in_use_title": "Все еще используется",
|
||||
"in_use_description": "Это видео прикреплено к урокам ниже. Откройте каждый урок, удалите оттуда видео, затем вернитесь, чтобы удалить его.",
|
||||
"confirm": "Удалить навсегда",
|
||||
"cancel": "Отмена",
|
||||
"close": "Закрыть"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -357,7 +357,11 @@
|
||||
"thumbnail_regen_failed": "Không thể tạo lại hình thu nhỏ",
|
||||
"transcript_saved": "Đã cập nhật bản ghi",
|
||||
"transcript_save_failed": "Không thể lưu bản ghi",
|
||||
"transcription_already_running": "Công việc chép lời đang được thực hiện cho video này"
|
||||
"transcription_already_running": "Công việc chép lời đang được thực hiện cho video này",
|
||||
"video_added_to_lesson": "Video đã được thêm vào bài học",
|
||||
"delete_success": "Đã xóa video",
|
||||
"delete_in_use": "Video này vẫn được đính kèm với một bài học. Loại bỏ nó ở đó đầu tiên.",
|
||||
"delete_failed": "Không thể xóa video"
|
||||
},
|
||||
"tags": {
|
||||
"group_created": "Đã tạo nhóm thẻ",
|
||||
@@ -2808,7 +2812,14 @@
|
||||
"exercise": "tập thể dục",
|
||||
"question": "Câu hỏi",
|
||||
"unknown": "Không xác định"
|
||||
}
|
||||
},
|
||||
"go_to_lesson": "Đi học",
|
||||
"slot_type": {
|
||||
"lesson_video": "Video bài học",
|
||||
"lesson_document": "Tài liệu bài học",
|
||||
"unknown": "Tệp đính kèm"
|
||||
},
|
||||
"refresh": "Làm mới mức sử dụng"
|
||||
},
|
||||
"form": {
|
||||
"file_name": "Tên tệp",
|
||||
@@ -2838,7 +2849,8 @@
|
||||
"edit": "Chỉnh sửa",
|
||||
"where_used": "Nơi sử dụng",
|
||||
"download": "Tải xuống",
|
||||
"manage_thumbnails": "Quản lý hình thu nhỏ"
|
||||
"manage_thumbnails": "Quản lý hình thu nhỏ",
|
||||
"delete": "Xóa"
|
||||
},
|
||||
"table": {
|
||||
"title": "Tiêu đề",
|
||||
@@ -2910,6 +2922,16 @@
|
||||
"custom_preview_alt": "Xem trước hình thu nhỏ tùy chỉnh",
|
||||
"regenerate": "tái sinh",
|
||||
"save_selection": "Lưu hình thu nhỏ"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Xóa video",
|
||||
"irreversible_title": "Việc này không thể hoàn tác được",
|
||||
"irreversible_description": "Video này và tất cả các tệp của nó — bản trình diễn, hình thu nhỏ và chú thích — sẽ bị xóa vĩnh viễn và không thể khôi phục được.",
|
||||
"in_use_title": "Vẫn đang được sử dụng",
|
||||
"in_use_description": "Video này được đính kèm với các bài học dưới đây. Mở từng bài học ra, xóa video ở đó rồi quay lại xóa.",
|
||||
"confirm": "Xóa vĩnh viễn",
|
||||
"cancel": "Hủy bỏ",
|
||||
"close": "Đóng"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -3,11 +3,13 @@ import './../bootstrap';
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
|
||||
import { runAnalyticsRollupDaily } from '@cio/analytics';
|
||||
import { purgeAssetStorage } from '@cio/core/services/assets/assets';
|
||||
import { pruneDeadLetterJobsOlderThan, reapStuckMediaJobs } from '@cio/db/queries';
|
||||
import {
|
||||
JOB_NAMES,
|
||||
QUEUE_NAMES,
|
||||
ZAnalyticsDailyRollupPayload,
|
||||
ZAssetStorageCleanupPayload,
|
||||
ZDeadLetterCleanupPayload,
|
||||
ZMediaJobReapPayload,
|
||||
ZRetentionCompactPayload,
|
||||
@@ -55,6 +57,17 @@ const worker = new Worker(
|
||||
return { reaped: reaped.length };
|
||||
}
|
||||
|
||||
if (job.name === JOB_NAMES.maintenance.assetStorageCleanup) {
|
||||
const data = ZAssetStorageCleanupPayload.parse(job.data ?? {});
|
||||
await purgeAssetStorage(data);
|
||||
log.info('asset-storage-cleanup-done', {
|
||||
assetId: data.assetId,
|
||||
prefixes: data.prefixes.length,
|
||||
keys: data.keys.length
|
||||
});
|
||||
return { assetId: data.assetId };
|
||||
}
|
||||
|
||||
if (job.name === JOB_NAMES.maintenance.analyticsDailyRollup) {
|
||||
const data = ZAnalyticsDailyRollupPayload.parse(job.data ?? {});
|
||||
const result = await runAnalyticsRollupDaily({ daysAgo: data.daysAgo });
|
||||
|
||||
Reference in New Issue
Block a user