mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 02:10:24 +00:00
add video details and extract button (#6908)
* add video details and extract button * update * update * update --------- Co-authored-by: zheng.shen <zheng.shen@seafile.com>
This commit is contained in:
@@ -124,7 +124,7 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail,
|
|||||||
);
|
);
|
||||||
|
|
||||||
let component = dom;
|
let component = dom;
|
||||||
if (Utils.imageCheck(dirent.name)) {
|
if (Utils.imageCheck(dirent.name) || Utils.videoCheck(dirent.name)) {
|
||||||
const fileDetails = getCellValueByColumn(record, { key: PRIVATE_COLUMN_KEY.FILE_DETAILS });
|
const fileDetails = getCellValueByColumn(record, { key: PRIVATE_COLUMN_KEY.FILE_DETAILS });
|
||||||
const fileDetailsJson = JSON.parse(fileDetails?.slice(9, -7) || '{}');
|
const fileDetailsJson = JSON.parse(fileDetails?.slice(9, -7) || '{}');
|
||||||
const fileLocation = getCellValueByColumn(record, { key: PRIVATE_COLUMN_KEY.LOCATION });
|
const fileLocation = getCellValueByColumn(record, { key: PRIVATE_COLUMN_KEY.LOCATION });
|
||||||
|
@@ -222,6 +222,14 @@ class MetadataManagerAPI {
|
|||||||
return this.req.post(url, params);
|
return this.req.post(url, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extractFileDetails = (repoID, objIds) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/extract-file-details/';
|
||||||
|
const params = {
|
||||||
|
obj_ids: objIds,
|
||||||
|
};
|
||||||
|
return this.req.post(url, params);
|
||||||
|
};
|
||||||
|
|
||||||
zipDownload(repoID, parent_dir, dirents) {
|
zipDownload(repoID, parent_dir, dirents) {
|
||||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/zip-task/';
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/zip-task/';
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
|
@@ -218,6 +218,11 @@ class Context {
|
|||||||
const lang = this.settings['lang'];
|
const lang = this.settings['lang'];
|
||||||
return this.metadataAPI.imageCaption(repoID, filePath, lang);
|
return this.metadataAPI.imageCaption(repoID, filePath, lang);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extractFileDetails = (objIds) => {
|
||||||
|
const repoID = this.settings['repoID'];
|
||||||
|
return this.metadataAPI.extractFileDetails(repoID, objIds);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Context;
|
export default Context;
|
||||||
|
@@ -31,3 +31,11 @@ export const getParentDirFromRecord = (record) => {
|
|||||||
export const getFileNameFromRecord = (record) => {
|
export const getFileNameFromRecord = (record) => {
|
||||||
return record ? record[PRIVATE_COLUMN_KEY.FILE_NAME] : '';
|
return record ? record[PRIVATE_COLUMN_KEY.FILE_NAME] : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const geRecordIdFromRecord = record => {
|
||||||
|
return record ? record[PRIVATE_COLUMN_KEY.ID] : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFileObjIdFromRecord = record => {
|
||||||
|
return record ? record[PRIVATE_COLUMN_KEY.OBJ_ID] : '';
|
||||||
|
};
|
||||||
|
@@ -3,6 +3,8 @@ export {
|
|||||||
getCellValueByColumn,
|
getCellValueByColumn,
|
||||||
getParentDirFromRecord,
|
getParentDirFromRecord,
|
||||||
getFileNameFromRecord,
|
getFileNameFromRecord,
|
||||||
|
geRecordIdFromRecord,
|
||||||
|
getFileObjIdFromRecord,
|
||||||
} from './core';
|
} from './core';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@@ -7,7 +7,9 @@ import { useMetadataView } from '../../../hooks/metadata-view';
|
|||||||
import { getColumnByKey, isNameColumn } from '../../../utils/column';
|
import { getColumnByKey, isNameColumn } from '../../../utils/column';
|
||||||
import { checkIsDir } from '../../../utils/row';
|
import { checkIsDir } from '../../../utils/row';
|
||||||
import { EVENT_BUS_TYPE, EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../../../constants';
|
import { EVENT_BUS_TYPE, EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../../../constants';
|
||||||
import { getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell';
|
import { getFileNameFromRecord, getParentDirFromRecord, getFileObjIdFromRecord,
|
||||||
|
geRecordIdFromRecord,
|
||||||
|
} from '../../../utils/cell';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -21,6 +23,8 @@ const OPERATION = {
|
|||||||
DELETE_RECORD: 'delete-record',
|
DELETE_RECORD: 'delete-record',
|
||||||
DELETE_RECORDS: 'delete-records',
|
DELETE_RECORDS: 'delete-records',
|
||||||
RENAME_FILE: 'rename-file',
|
RENAME_FILE: 'rename-file',
|
||||||
|
FILE_DETAIL: 'file-detail',
|
||||||
|
FILE_DETAILS: 'file-details',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContextMenu = (props) => {
|
const ContextMenu = (props) => {
|
||||||
@@ -74,6 +78,13 @@ const ContextMenu = (props) => {
|
|||||||
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete selected'), records: ableDeleteRecords });
|
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete selected'), records: ableDeleteRecords });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageOrVideoRecords = records.filter(record => {
|
||||||
|
const fileName = getFileNameFromRecord(record);
|
||||||
|
return Utils.imageCheck(fileName) || Utils.videoCheck(fileName);
|
||||||
|
});
|
||||||
|
if (imageOrVideoRecords.length > 0) {
|
||||||
|
list.push({ value: OPERATION.FILE_DETAILS, label: gettext('Extract file details'), records: imageOrVideoRecords });
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +103,13 @@ const ContextMenu = (props) => {
|
|||||||
if (ableDeleteRecords.length > 0) {
|
if (ableDeleteRecords.length > 0) {
|
||||||
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords });
|
list.push({ value: OPERATION.DELETE_RECORDS, label: gettext('Delete'), records: ableDeleteRecords });
|
||||||
}
|
}
|
||||||
|
const imageOrVideoRecords = records.filter(record => {
|
||||||
|
const fileName = getFileNameFromRecord(record);
|
||||||
|
return Utils.imageCheck(fileName) || Utils.videoCheck(fileName);
|
||||||
|
});
|
||||||
|
if (imageOrVideoRecords.length > 0) {
|
||||||
|
list.push({ value: OPERATION.FILE_DETAILS, label: gettext('Extract file details'), records: imageOrVideoRecords });
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,15 +125,21 @@ const ContextMenu = (props) => {
|
|||||||
const isFolder = checkIsDir(record);
|
const isFolder = checkIsDir(record);
|
||||||
list.push({ value: OPERATION.OPEN_IN_NEW_TAB, label: isFolder ? gettext('Open folder in new tab') : gettext('Open file in new tab'), record });
|
list.push({ value: OPERATION.OPEN_IN_NEW_TAB, label: isFolder ? gettext('Open folder in new tab') : gettext('Open file in new tab'), record });
|
||||||
list.push({ value: OPERATION.OPEN_PARENT_FOLDER, label: gettext('Open parent folder'), record });
|
list.push({ value: OPERATION.OPEN_PARENT_FOLDER, label: gettext('Open parent folder'), record });
|
||||||
|
const fileName = getFileNameFromRecord(record);
|
||||||
|
|
||||||
if (descriptionColumn) {
|
if (descriptionColumn) {
|
||||||
if (checkIsDescribableDoc(record)) {
|
if (checkIsDescribableDoc(record)) {
|
||||||
list.push({ value: OPERATION.GENERATE_DESCRIPTION, label: gettext('Generate description'), record });
|
list.push({ value: OPERATION.GENERATE_DESCRIPTION, label: gettext('Generate description'), record });
|
||||||
} else if (canModifyRow && Utils.imageCheck(getFileNameFromRecord(record))) {
|
} else if (canModifyRow && Utils.imageCheck(fileName)) {
|
||||||
list.push({ value: OPERATION.IMAGE_CAPTION, label: gettext('Generate image description'), record });
|
list.push({ value: OPERATION.IMAGE_CAPTION, label: gettext('Generate image description'), record });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canModifyRow && (Utils.imageCheck(fileName) || Utils.videoCheck(fileName))) {
|
||||||
|
list.push({ value: OPERATION.FILE_DETAIL, label: gettext('Extract file detail'), record: record });
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle delete folder/file
|
||||||
if (canDeleteRow) {
|
if (canDeleteRow) {
|
||||||
list.push({ value: OPERATION.DELETE_RECORD, label: isFolder ? gettext('Delete folder') : gettext('Delete file'), record });
|
list.push({ value: OPERATION.DELETE_RECORD, label: isFolder ? gettext('Delete folder') : gettext('Delete file'), record });
|
||||||
}
|
}
|
||||||
@@ -220,6 +244,40 @@ const ContextMenu = (props) => {
|
|||||||
});
|
});
|
||||||
}, [updateRecords]);
|
}, [updateRecords]);
|
||||||
|
|
||||||
|
const updateFileDetails = useCallback((records) => {
|
||||||
|
const recordObjIds = records.map(record => getFileObjIdFromRecord(record));
|
||||||
|
if (recordObjIds.length > 50) {
|
||||||
|
toaster.danger(gettext('Select up to 50 files'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordIds = records.map(record => geRecordIdFromRecord(record));
|
||||||
|
window.sfMetadataContext.extractFileDetails(recordObjIds).then(res => {
|
||||||
|
const captureColumn = getColumnByKey(metadata.columns, PRIVATE_COLUMN_KEY.CAPTURE_TIME);
|
||||||
|
|
||||||
|
if (captureColumn) {
|
||||||
|
let idOldRecordData = {};
|
||||||
|
let idOriginalOldRecordData = {};
|
||||||
|
const captureColumnKey = PRIVATE_COLUMN_KEY.CAPTURE_TIME;
|
||||||
|
records.forEach(record => {
|
||||||
|
idOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [captureColumnKey]: record[captureColumnKey] };
|
||||||
|
idOriginalOldRecordData[record[PRIVATE_COLUMN_KEY.ID]] = { [captureColumnKey]: record[captureColumnKey] };
|
||||||
|
});
|
||||||
|
let idRecordUpdates = {};
|
||||||
|
let idOriginalRecordUpdates = {};
|
||||||
|
res.data.details.forEach(detail => {
|
||||||
|
const updateRecordId = detail[PRIVATE_COLUMN_KEY.ID];
|
||||||
|
idRecordUpdates[updateRecordId] = { [captureColumnKey]: detail[captureColumnKey] };
|
||||||
|
idOriginalRecordUpdates[updateRecordId] = { [captureColumnKey]: detail[captureColumnKey] };
|
||||||
|
});
|
||||||
|
updateRecords({ recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData });
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMessage = gettext('Failed to extract file details');
|
||||||
|
toaster.danger(errorMessage);
|
||||||
|
});
|
||||||
|
}, [metadata, updateRecords]);
|
||||||
|
|
||||||
const handleOptionClick = useCallback((event, option) => {
|
const handleOptionClick = useCallback((event, option) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
switch (option.value) {
|
switch (option.value) {
|
||||||
@@ -283,12 +341,22 @@ const ContextMenu = (props) => {
|
|||||||
window.sfMetadataContext.eventBus.dispatch(METADATA_EVENT_BUS_TYPE.OPEN_EDITOR);
|
window.sfMetadataContext.eventBus.dispatch(METADATA_EVENT_BUS_TYPE.OPEN_EDITOR);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION.FILE_DETAILS: {
|
||||||
|
const { records } = option;
|
||||||
|
updateFileDetails(records);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OPERATION.FILE_DETAIL: {
|
||||||
|
const { record } = option;
|
||||||
|
updateFileDetails([record]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected, generateDescription, imageCaption, selectNone, deleteRecords, toggleDeleteFolderDialog]);
|
}, [onOpenFileInNewTab, onOpenParentFolder, onCopySelected, onClearSelected, generateDescription, imageCaption, selectNone, deleteRecords, toggleDeleteFolderDialog, updateFileDetails]);
|
||||||
|
|
||||||
const getMenuPosition = useCallback((x = 0, y = 0) => {
|
const getMenuPosition = useCallback((x = 0, y = 0) => {
|
||||||
let menuStyles = {
|
let menuStyles = {
|
||||||
|
@@ -13,7 +13,7 @@ from seahub.api2.authentication import TokenAuthentication
|
|||||||
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
|
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
|
||||||
from seahub.views import check_folder_permission
|
from seahub.views import check_folder_permission
|
||||||
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
||||||
get_unmodifiable_columns, can_read_metadata, init_faces, add_init_face_recognition_task, get_metadata_by_faces
|
get_unmodifiable_columns, can_read_metadata, init_faces, add_init_face_recognition_task, get_metadata_by_faces, extract_file_details
|
||||||
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
||||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||||
from seahub.utils.repo import is_repo_admin
|
from seahub.utils.repo import is_repo_admin
|
||||||
@@ -260,6 +260,8 @@ class MetadataRecords(APIView):
|
|||||||
parameters = []
|
parameters = []
|
||||||
for record_data in records_data:
|
for record_data in records_data:
|
||||||
record = record_data.get('record', {})
|
record = record_data.get('record', {})
|
||||||
|
if not record:
|
||||||
|
continue
|
||||||
record_id = record_data.get('record_id', '')
|
record_id = record_data.get('record_id', '')
|
||||||
if not record_id:
|
if not record_id:
|
||||||
error_msg = 'record_id invalid.'
|
error_msg = 'record_id invalid.'
|
||||||
@@ -272,6 +274,9 @@ class MetadataRecords(APIView):
|
|||||||
sql = sql.rstrip('OR ')
|
sql = sql.rstrip('OR ')
|
||||||
sql += ';'
|
sql += ';'
|
||||||
|
|
||||||
|
if not parameters:
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query_result = metadata_server_api.query_rows(sql, parameters)
|
query_result = metadata_server_api.query_rows(sql, parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1061,3 +1066,42 @@ class FaceRecognitionManage(APIView):
|
|||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
|
|
||||||
return Response({'task_id': task_id})
|
return Response({'task_id': task_id})
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataExtractFileDetails(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request, repo_id):
|
||||||
|
obj_ids = request.data.get('obj_ids')
|
||||||
|
if not obj_ids or not isinstance(obj_ids, list) or len(obj_ids) > 50:
|
||||||
|
error_msg = 'obj_ids is invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not record or not record.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if permission != 'rw':
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'obj_ids': obj_ids,
|
||||||
|
'repo_id': repo_id
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
resp = extract_file_details(params=params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
|
|
||||||
|
return Response({'details': resp})
|
||||||
|
@@ -56,6 +56,15 @@ def get_metadata_by_faces(faces, metadata_server_api):
|
|||||||
return query_result
|
return query_result
|
||||||
|
|
||||||
|
|
||||||
|
def extract_file_details(params):
|
||||||
|
payload = {'exp': int(time.time()) + 300, }
|
||||||
|
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
|
||||||
|
headers = {"Authorization": "Token %s" % token}
|
||||||
|
url = urljoin(SEAFEVENTS_SERVER_URL, '/extract-file-details')
|
||||||
|
resp = requests.post(url, json=params, headers=headers, timeout=30)
|
||||||
|
return json.loads(resp.content)['details']
|
||||||
|
|
||||||
|
|
||||||
def generator_base64_code(length=4):
|
def generator_base64_code(length=4):
|
||||||
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'
|
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
ids = random.sample(possible, length)
|
ids = random.sample(possible, length)
|
||||||
|
@@ -212,7 +212,7 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView,
|
|||||||
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
||||||
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
||||||
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||||
FaceRecognitionManage, FacesRecord
|
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails
|
||||||
from seahub.api2.endpoints.user_list import UserListView
|
from seahub.api2.endpoints.user_list import UserListView
|
||||||
from seahub.api2.endpoints.seahub_io import SeahubIOStatus
|
from seahub.api2.endpoints.seahub_io import SeahubIOStatus
|
||||||
|
|
||||||
@@ -1056,6 +1056,7 @@ if settings.ENABLE_METADATA_MANAGEMENT:
|
|||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-record/$', FacesRecord.as_view(), name='api-v2.1-metadata-face-record'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-record/$', FacesRecord.as_view(), name='api-v2.1-metadata-face-record'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-records/$', FacesRecords.as_view(), name='api-v2.1-metadata-face-records'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-records/$', FacesRecords.as_view(), name='api-v2.1-metadata-face-records'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/extract-file-details/$', MetadataExtractFileDetails.as_view(), name='api-v2.1-metadata-extract-file-details'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ai API
|
# ai API
|
||||||
|
Reference in New Issue
Block a user