Source code for flexicon.code.Shared.MediaOperations
#
# MediaOperations.py
#
# Class: MediaOperations
# Media file operations for FieldWorks Language Explorer
# projects via SIL Language and Culture Model (LCM) API.
#
# Platform: Python.NET
# FieldWorks Version 9+
#
# Copyright 2025
#
import logging
logger = logging.getLogger(__name__)
import os
import shutil
# Import FLEx LCM types
from SIL.LCModel import (
ICmFile,
ICmFileFactory,
ICmFolder,
ICmFolderFactory,
ICmObjectRepository,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
# Import flexlibs exceptions
from ..FLExProject import (
FP_ParameterError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
# --- Media Type Enum ---
# --- MediaOperations Class ---
[docs]
class MediaOperations(BaseOperations):
"""
Provides operations for managing media files in a FLEx project.
Media files include audio recordings, videos, and images that can be
attached to lexical entries, pronunciations, examples, and other objects.
This class handles both the database references (ICmFile) and file system
operations for project media.
This class should be accessed via FLExProject.Media property.
Example:
>>> project = FLExProject()
>>> project.OpenProject("MyProject", writeEnabled=True)
>>> # Get all media files
>>> for media in project.Media.GetAll():
... path = project.Media.GetInternalPath(media)
... print(path)
>>> # Add a new media file
>>> media = project.Media.Create("/path/to/audio.wav")
>>> # Check media type
>>> if project.Media.IsAudio(media):
... print("This is an audio file")
>>> project.CloseProject()
"""
def __init__(self, project):
"""
Initialize MediaOperations.
Args:
project: FLExProject instance
"""
super().__init__(project)
def __WSHandle(self, wsHandle):
"""
Internal helper to resolve writing system handle.
Args:
wsHandle: Writing system handle (language tag or ID), or None for default
Returns:
int: Resolved writing system handle
"""
if wsHandle is None:
return self.project.project.DefaultAnalWs
return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs)
# --- Core CRUD Operations ---
@wrap_enumerable
@OperationsMethod
def GetAll(self):
"""
Retrieve all media files in the FLEx project.
This method returns an iterator over all ICmFile objects in the
project database, allowing iteration over the complete media inventory.
Yields:
ICmFile: Each media file object in the project
Example:
>>> for media in project.Media.GetAll():
... path = project.Media.GetInternalPath(media)
... label = project.Media.GetLabel(media, "en")
... print(f"{label}: {path}")
Recording1: audio/recording1.wav
Video1: videos/intro.mp4
Photo1: images/photo1.jpg
Notes:
- Returns an iterator for memory efficiency with large projects
- Media files are returned in database order
- Use GetInternalPath() to access the file path
- Includes all media types (audio, video, image)
See Also:
Find, Create, GetMediaType
"""
return self.project.ObjectsIn(ICmObjectRepository, ICmFile)
@OperationsMethod
def Create(self, file_path, label=None, wsHandle=None):
"""
Create a new media file reference in the FLEx project.
Args:
file_path: The file path (internal to project or external)
label: Optional descriptive label for the media file
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
ICmFile: The newly created media file object
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If file_path is None
FP_ParameterError: If file_path is empty
Example:
>>> # Create media file reference
>>> media = project.Media.Create("LinkedFiles/AudioVisual/audio.wav")
>>> print(project.Media.GetInternalPath(media))
LinkedFiles/AudioVisual/audio.wav
>>> # Create with label
>>> media = project.Media.Create("audio/recording.wav",
... label="Speaker 1")
>>> print(project.Media.GetLabel(media, "en"))
Speaker 1
Notes:
- Creates an ICmFile object in the database
- The file path is stored as-is (not validated)
- File is NOT copied to project - only reference is created
- Use CopyToProject() to import external files
- Label is optional and can be set later with SetLabel()
See Also:
Delete, CopyToProject, SetInternalPath
"""
self._EnsureWriteEnabled()
self._ValidateParam(file_path, "file_path")
if not file_path or not file_path.strip():
raise FP_ParameterError("File path cannot be empty")
wsHandle = self.__WSHandle(wsHandle)
# Get the factory and create the media file
factory = self.project.project.ServiceLocator.GetService(ICmFileFactory)
new_media = factory.Create()
# Set the internal path
new_media.InternalPath = file_path.strip()
# Set label if provided
if label:
mkstr = TsStringUtils.MakeString(label, wsHandle)
new_media.Description.set_String(wsHandle, mkstr)
return new_media
@OperationsMethod
def Delete(self, media_or_hvo):
"""
Delete a media file reference from the FLEx project.
Args:
media_or_hvo: Either an ICmFile object or its HVO (database ID)
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> if media:
... project.Media.Delete(media)
>>> # Delete by HVO
>>> project.Media.Delete(12345)
Warning:
- This is a destructive operation
- Removes the database reference only
- Does NOT delete the actual file from disk
- References from other objects will be removed
- Cannot be undone
Notes:
- Only removes the ICmFile object from database
- The physical file remains in the file system
- Use os.remove() separately if you want to delete the file
- All references to this media from other objects are cleaned up
See Also:
Create, Exists
"""
self._EnsureWriteEnabled()
self._ValidateParam(media_or_hvo, "media_or_hvo")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
# Delete the object (this removes all references)
# Note: LCModel handles cascading deletion of references
self.project.cache.DomainDataByFlid.DeleteObj(media.Hvo)
@OperationsMethod
def Duplicate(self, item_or_hvo, deep=False):
"""
Duplicate a media file reference, creating a new reference to the same file.
This method creates a copy of the media file reference (ICmFile object) in
the database. The duplicate points to the SAME physical file - the file
itself is NOT duplicated on disk. Use copy_file=True to create a physical
copy of the file.
Args:
item_or_hvo: Either an ICmFile object or its HVO (database ID)
deep (bool): If True, the physical file is also copied with a new name.
If False, only the database reference is duplicated (both references
point to the same file).
Returns:
ICmFile: The newly created duplicate media file reference
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If item_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> # Shallow duplicate (reference only, same file)
>>> media = project.Media.Find("audio.wav")
>>> duplicate = project.Media.Duplicate(media, deep=False)
>>> print(project.Media.GetInternalPath(duplicate))
LinkedFiles/AudioVisual/audio.wav
>>> print(project.Media.GetLabel(duplicate, "en"))
Speaker 1 (copy)
>>> # Deep duplicate (copy the physical file too)
>>> duplicate = project.Media.Duplicate(media, deep=True)
>>> print(project.Media.GetInternalPath(duplicate))
LinkedFiles/AudioVisual/audio_copy.wav
Warning:
- With deep=False, both references point to the SAME file
- Deleting the file affects both references
- With deep=True, the file is physically copied (doubles disk space)
- The duplicate will have a " (copy)" suffix in the label
Notes:
- Duplicated reference is added to the project database
- New GUID is auto-generated for the duplicate
- Internal path is copied (or modified if deep=True)
- Label is copied with " (copy)" suffix
- With deep=False, both references share the same physical file
- With deep=True, a new file is created with "_copy" suffix
See Also:
Create, Delete, CopyToProject
"""
self._EnsureWriteEnabled()
self._ValidateParam(item_or_hvo, "item_or_hvo")
# Resolve to media object
if isinstance(item_or_hvo, int):
source_media = self.project.Object(item_or_hvo)
if not isinstance(source_media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
source_media = item_or_hvo
# Get source properties
wsHandle = self.__WSHandle(None)
internal_path = self.GetInternalPath(source_media)
label = self.GetLabel(source_media, wsHandle)
# Create unique label by appending " (copy)"
new_label = f"{label} (copy)" if label else "Media (copy)"
# Handle deep copy (copy the physical file)
new_path = internal_path
if deep and internal_path:
# Get the external path
external_path = self.GetExternalPath(source_media)
if os.path.exists(external_path):
# Create new filename with "_copy" suffix
dir_name = os.path.dirname(internal_path)
file_name = os.path.basename(internal_path)
name_base, ext = os.path.splitext(file_name)
new_file_name = f"{name_base}_copy{ext}"
new_path = os.path.join(dir_name, new_file_name)
# Ensure uniqueness
counter = 1
while True:
new_external_path = os.path.join(os.path.dirname(external_path), os.path.basename(new_path))
if not os.path.exists(new_external_path):
break
new_file_name = f"{name_base}_copy{counter}{ext}"
new_path = os.path.join(dir_name, new_file_name)
counter += 1
# Copy the file
try:
shutil.copy2(external_path, new_external_path)
except Exception as e:
logger.warning(f"Failed to copy physical file: {e}")
# Continue with shallow copy if file copy fails
new_path = internal_path
# Create the new media reference
new_media = self.Create(new_path, label=new_label, wsHandle=wsHandle)
# Copy full Description MultiString (all writing systems)
# Create() only sets a single-WS label; CopyAlternatives preserves all WS
if hasattr(source_media, "Description") and source_media.Description:
new_media.Description.CopyAlternatives(source_media.Description)
return new_media
# ========== SYNC INTEGRATION METHODS ==========
@OperationsMethod
def GetSyncableProperties(self, item):
"""
Get all syncable properties of a media file.
Args:
item: The ICmFile object.
Returns:
dict: Dictionary of syncable properties with their values.
Example:
>>> props = project.Media.GetSyncableProperties(media)
>>> print(props['InternalPath'])
'LinkedFiles/AudioVisual/audio.wav'
>>> print(props['Description'])
{'en': 'Speaker 1 recording'}
Notes:
- InternalPath is a string property
- Description is a MultiString property (dict with WS keys)
- Does NOT include file content or metadata
"""
props = {}
# String property - InternalPath
if hasattr(item, "InternalPath") and item.InternalPath:
props["InternalPath"] = item.InternalPath
# MultiString property - Description
if hasattr(item, "Description") and item.Description:
props["Description"] = self.project.GetMultiStringDict(item.Description)
return props
@OperationsMethod
def CompareTo(self, item1, item2, ops1=None, ops2=None):
"""
Compare two media files for differences.
Args:
item1: First media object (from project 1)
item2: Second media object (from project 2)
ops1: Optional MediaOperations instance for project 1 (defaults to self)
ops2: Optional MediaOperations instance for project 2 (defaults to self)
Returns:
tuple: (is_different, differences_dict)
- is_different (bool): True if media files differ, False if identical
- differences_dict (dict): Maps property names to (value1, value2) tuples
Example:
>>> is_diff, diffs = ops1.CompareTo(media1, media2, ops1, ops2)
>>> if is_diff:
... for prop, (val1, val2) in diffs.items():
... print(f"{prop}: {val1} != {val2}")
Notes:
- Compares InternalPath and Description
- MultiStrings are compared across all writing systems
- Empty/null values are treated as equivalent
"""
if ops1 is None:
ops1 = self
if ops2 is None:
ops2 = self
props1 = ops1.GetSyncableProperties(item1)
props2 = ops2.GetSyncableProperties(item2)
differences = {}
# Get all property keys from both items
all_keys = set(props1.keys()) | set(props2.keys())
for key in all_keys:
val1 = props1.get(key)
val2 = props2.get(key)
# Compare values
if self.project._CompareValues(val1, val2):
# Values are different
differences[key] = (val1, val2)
is_different = len(differences) > 0
return (is_different, differences)
@OperationsMethod
def Find(self, filename):
"""
Find a media file by its filename (internal path).
Args:
filename: The filename or path to search for (case-sensitive)
Returns:
ICmFile or None: The media file object if found, None otherwise
Raises:
FP_NullParameterError: If filename is None
Example:
>>> media = project.Media.Find("audio.wav")
>>> if media:
... print(f"Found: {project.Media.GetInternalPath(media)}")
Found: LinkedFiles/AudioVisual/audio.wav
>>> # Search with full path
>>> media = project.Media.Find("LinkedFiles/AudioVisual/audio.wav")
Notes:
- Returns first match only
- Search is case-sensitive
- Searches InternalPath field
- Can search by filename or full path
- Returns None if not found (doesn't raise exception)
- Matches exact path or ending path component
See Also:
Exists, GetAll, GetInternalPath
"""
self._ValidateParam(filename, "filename")
if not filename or not filename.strip():
return None
filename = filename.strip()
# Search through all media files
for media in self.GetAll():
internal_path = media.InternalPath or ""
# Match exact path or filename at end of path
if internal_path == filename or internal_path.endswith(os.sep + filename):
return media
return None
@OperationsMethod
def Exists(self, filename):
"""
Check if a media file exists in the FLEx project.
Args:
filename: The filename or path to check
Returns:
bool: True if the media file exists, False otherwise
Example:
>>> if project.Media.Exists("audio.wav"):
... media = project.Media.Find("audio.wav")
... else:
... media = project.Media.Create("audio.wav")
Notes:
- Search is case-sensitive
- Returns False for empty or whitespace-only filenames
- Use Find() to get the actual media object
See Also:
Find, Create
"""
if not filename or not filename.strip():
return False
return self.Find(filename) is not None
# --- File Properties ---
@OperationsMethod
def GetInternalPath(self, media_or_hvo):
"""
Get the internal path of a media file.
The internal path is the file location relative to the project's
LinkedFiles directory or an absolute path.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
str: The internal path (empty string if not set)
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> path = project.Media.GetInternalPath(media)
>>> print(path)
LinkedFiles/AudioVisual/audio.wav
Notes:
- Returns empty string if path not set
- Path may be relative or absolute
- Use GetExternalPath() to resolve to full system path
- Path uses forward slashes on all platforms
See Also:
SetInternalPath, GetExternalPath
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
return media.InternalPath or ""
@OperationsMethod
def GetExternalPath(self, media_or_hvo):
"""
Get the external (absolute) path of a media file.
This resolves the internal path to an absolute file system path,
relative to the project's LinkedFiles directory if needed.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
str: The absolute file path (may not exist on disk)
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> path = project.Media.GetExternalPath(media)
>>> print(path)
C:/Users/Me/Documents/FieldWorks/Projects/MyProject/LinkedFiles/AudioVisual/audio.wav
>>> # Check if file exists on disk
>>> import os
>>> if os.path.exists(path):
... print("File exists")
Notes:
- Returns absolute path, resolved from internal path
- If internal path is absolute, returns it as-is
- If relative, resolves relative to project LinkedFiles directory
- Does NOT verify that file exists on disk
- Use IsValid() to check if file exists
See Also:
GetInternalPath, IsValid
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
internal_path = media.InternalPath or ""
# If already absolute, return as-is
if os.path.isabs(internal_path):
return internal_path
# Otherwise, resolve relative to project LinkedFiles directory
# Note: This assumes project has a LinkedFiles directory
# In actual FLEx, this would use the project's LinkedFilesRootDir
if hasattr(self.project.project, "LinkedFilesRootDir"):
linked_files_dir = self.project.project.LinkedFilesRootDir
return os.path.join(linked_files_dir, internal_path)
# Fallback: return internal path if we can't resolve
return internal_path
@OperationsMethod
def SetInternalPath(self, media_or_hvo, path):
"""
Set the internal path of a media file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
path: The new internal path to set
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If media_or_hvo or path is None
FP_ParameterError: If path is empty or media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> project.Media.SetInternalPath(media,
... "LinkedFiles/AudioVisual/new_audio.wav")
>>> print(project.Media.GetInternalPath(media))
LinkedFiles/AudioVisual/new_audio.wav
Notes:
- Updates the internal path reference
- Does NOT move or rename the actual file
- Path can be relative or absolute
- Use forward slashes for cross-platform compatibility
See Also:
GetInternalPath, Create
"""
self._EnsureWriteEnabled()
self._ValidateParam(media_or_hvo, "media_or_hvo")
self._ValidateParam(path, "path")
if not path or not path.strip():
raise FP_ParameterError("Path cannot be empty")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
media.InternalPath = path.strip()
@OperationsMethod
def RenameMediaFile(self, media_or_hvo, new_filename):
"""
Rename a media file on disk and update the database reference.
This is an atomic operation that:
1. Validates parameters and checks write permissions
2. Gets current internal and external paths
3. Constructs new paths with new filename
4. Renames the physical file in LinkedFiles directory
5. Updates the InternalPath in the database
6. Handles filename conflicts (adds suffix if needed)
Args:
media_or_hvo: Either an ICmFile object or its HVO (database ID)
new_filename: New filename (without path, just filename.ext)
Returns:
str: The actual new internal path (may differ if conflict occurred)
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If media_or_hvo or new_filename is None
FP_ParameterError: If media doesn't exist, filename is invalid,
or contains path separators
OSError: If file doesn't exist or rename operation fails
Example:
>>> # Rename an audio file
>>> media = project.Media.Find("LinkedFiles/AudioVisual/old_name.wav")
>>> new_path = project.Media.RenameMediaFile(media, "new_name.wav")
>>> print(new_path)
LinkedFiles/AudioVisual/new_name.wav
>>> # Handles conflicts automatically
>>> media2 = project.Media.Find("LinkedFiles/AudioVisual/temp.wav")
>>> new_path = project.Media.RenameMediaFile(media2, "new_name.wav")
>>> print(new_path)
LinkedFiles/AudioVisual/new_name_1.wav
>>> # Rename by HVO
>>> new_path = project.Media.RenameMediaFile(12345, "recording.wav")
Warning:
- Renames the physical file on disk
- This is a destructive operation
- If new filename already exists, adds _1, _2, etc. suffix
- Cannot be undone
Notes:
- Preserves the subdirectory (AudioVisual, Pictures, etc.)
- Only changes the filename, not the directory path
- Handles filename conflicts by adding _1, _2, etc.
- Updates InternalPath in database automatically
- Old file must exist on disk
- New filename cannot contain path separators (/ or \\)
- Validates that new_filename is just a filename, not a path
See Also:
SetInternalPath, CopyToProject, GetExternalPath, GetInternalPath
"""
# Check write permissions
self._EnsureWriteEnabled()
# Validate parameters
self._ValidateParam(media_or_hvo, "media_or_hvo")
self._ValidateParam(new_filename, "new_filename")
if not new_filename or not new_filename.strip():
raise FP_ParameterError("New filename cannot be empty")
new_filename = new_filename.strip()
# Validate that new_filename is just a filename, not a path
if os.sep in new_filename or "/" in new_filename or "\\" in new_filename:
raise FP_ParameterError(f"New filename cannot contain path separators: {new_filename}")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
# Get current paths
old_internal_path = self.GetInternalPath(media)
if not old_internal_path:
raise FP_ParameterError("Media file has no internal path set")
old_external_path = self.GetExternalPath(media)
# Check if old file exists
if not os.path.exists(old_external_path):
logger.error(f"File not found: {old_external_path}")
raise FP_ParameterError(f"File not found: {old_external_path}")
# Construct new path (preserve directory)
dir_name = os.path.dirname(old_internal_path)
new_internal_path = os.path.join(dir_name, new_filename)
# Get the external directory path for constructing new external path
old_external_dir = os.path.dirname(old_external_path)
new_external_path = os.path.join(old_external_dir, new_filename)
# Handle conflicts - check if target file already exists
if os.path.exists(new_external_path):
base, ext = os.path.splitext(new_filename)
counter = 1
while True:
new_filename_conflict = f"{base}_{counter}{ext}"
new_internal_path = os.path.join(dir_name, new_filename_conflict)
new_external_path = os.path.join(old_external_dir, new_filename_conflict)
if not os.path.exists(new_external_path):
break
counter += 1
logger.warning(f"File {new_filename} already exists, using {new_filename_conflict}")
# Rename the physical file
try:
os.rename(old_external_path, new_external_path)
logger.info(f"Renamed media file: {old_external_path} -> {new_external_path}")
except OSError as e:
logger.error(f"Failed to rename file: {e}")
raise
# Update database reference
try:
self.SetInternalPath(media, new_internal_path)
logger.info(f"Updated internal path: {old_internal_path} -> {new_internal_path}")
except Exception as e:
# If database update fails, try to roll back the file rename
logger.error(f"Failed to update database, attempting rollback: {e}")
try:
os.rename(new_external_path, old_external_path)
logger.info(f"Rolled back file rename")
except OSError as rollback_error:
logger.error(f"Rollback failed: {rollback_error}")
raise
return new_internal_path
@OperationsMethod
def GetLabel(self, media_or_hvo, wsHandle=None):
"""
Get the label/description of a media file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
str: The label text (empty string if not set)
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> label = project.Media.GetLabel(media, "en")
>>> print(label)
Speaker 1 pronunciation
>>> # Get in specific writing system
>>> label_fr = project.Media.GetLabel(media, "fr")
Notes:
- Returns empty string if label not set
- Returns empty string if label not set in specified writing system
- Labels are multi-string (can have different text per WS)
- Default writing system is analysis WS
See Also:
SetLabel
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
wsHandle = self.__WSHandle(wsHandle)
# Get the label/description string
if hasattr(media, "Description"):
label = ITsString(media.Description.get_String(wsHandle)).Text
return label or ""
return ""
@OperationsMethod
def SetLabel(self, media_or_hvo, text, wsHandle=None):
"""
Set the label/description of a media file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
text: The label text to set
wsHandle: Optional writing system handle. Defaults to analysis WS.
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If media_or_hvo or text is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> project.Media.SetLabel(media, "Speaker 1 pronunciation", "en")
>>> print(project.Media.GetLabel(media, "en"))
Speaker 1 pronunciation
>>> # Set in multiple writing systems
>>> project.Media.SetLabel(media, "Pronunciation locuteur 1", "fr")
Notes:
- Text can be empty string to clear the label
- Label is stored in the specified writing system
- Does not affect other writing systems
- Labels help identify media files in the UI
See Also:
GetLabel
"""
self._EnsureWriteEnabled()
self._ValidateParam(media_or_hvo, "media_or_hvo")
self._ValidateParam(text, "text")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
wsHandle = self.__WSHandle(wsHandle)
# Set the label/description string
if hasattr(media, "Description"):
mkstr = TsStringUtils.MakeString(text, wsHandle)
media.Description.set_String(wsHandle, mkstr)
# --- Media Type Detection ---
@OperationsMethod
def GetMediaType(self, media_or_hvo):
"""
Get the media type (audio/video/image) based on file extension.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
int: Media type constant (MediaType.AUDIO, VIDEO, IMAGE, or UNKNOWN)
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> media_type = project.Media.GetMediaType(media)
>>> if media_type == MediaType.AUDIO:
... print("This is an audio file")
This is an audio file
>>> # Check type name
>>> type_names = {MediaType.AUDIO: "Audio",
... MediaType.VIDEO: "Video",
... MediaType.IMAGE: "Image",
... MediaType.UNKNOWN: "Unknown"}
>>> print(type_names[media_type])
Audio
Notes:
- Detection is based on file extension only
- Audio extensions: .wav, .mp3, .ogg, .wma, .m4a, .flac
- Video extensions: .mp4, .avi, .mov, .wmv, .mpg, .mpeg
- Image extensions: .jpg, .jpeg, .png, .gif, .bmp, .tif, .tiff
- Returns MediaType.UNKNOWN for unrecognized extensions
- Does not verify actual file format
See Also:
IsAudio, IsVideo, IsImage
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
internal_path = media.InternalPath or ""
if not internal_path:
return MediaType.UNKNOWN
# Get file extension
_, ext = os.path.splitext(internal_path.lower())
# Define media type by extension
audio_exts = {".wav", ".mp3", ".ogg", ".wma", ".m4a", ".flac", ".aac", ".opus"}
video_exts = {".mp4", ".avi", ".mov", ".wmv", ".mpg", ".mpeg", ".mkv", ".webm"}
image_exts = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff", ".svg"}
if ext in audio_exts:
return MediaType.AUDIO
elif ext in video_exts:
return MediaType.VIDEO
elif ext in image_exts:
return MediaType.IMAGE
else:
return MediaType.UNKNOWN
@OperationsMethod
def IsAudio(self, media_or_hvo):
"""
Check if a media file is an audio file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
bool: True if the file is audio, False otherwise
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> if project.Media.IsAudio(media):
... print("This is an audio file")
This is an audio file
>>> # Process all audio files
>>> for media in project.Media.GetAll():
... if project.Media.IsAudio(media):
... # Process audio file
... pass
Notes:
- Detection is based on file extension
- Audio extensions: .wav, .mp3, .ogg, .wma, .m4a, .flac, .aac, .opus
- Does not verify actual file format
See Also:
GetMediaType, IsVideo, IsImage
"""
return self.GetMediaType(media_or_hvo) == MediaType.AUDIO
@OperationsMethod
def IsVideo(self, media_or_hvo):
"""
Check if a media file is a video file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
bool: True if the file is video, False otherwise
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("intro.mp4")
>>> if project.Media.IsVideo(media):
... print("This is a video file")
This is a video file
>>> # Count video files
>>> video_count = sum(1 for m in project.Media.GetAll()
... if project.Media.IsVideo(m))
>>> print(f"Total videos: {video_count}")
Notes:
- Detection is based on file extension
- Video extensions: .mp4, .avi, .mov, .wmv, .mpg, .mpeg, .mkv, .webm
- Does not verify actual file format
See Also:
GetMediaType, IsAudio, IsImage
"""
return self.GetMediaType(media_or_hvo) == MediaType.VIDEO
@OperationsMethod
def IsImage(self, media_or_hvo):
"""
Check if a media file is an image file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
bool: True if the file is image, False otherwise
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("photo.jpg")
>>> if project.Media.IsImage(media):
... print("This is an image file")
This is an image file
>>> # Get all image files
>>> images = [m for m in project.Media.GetAll()
... if project.Media.IsImage(m)]
Notes:
- Detection is based on file extension
- Image extensions: .jpg, .jpeg, .png, .gif, .bmp, .tif, .tiff, .svg
- Does not verify actual file format
See Also:
GetMediaType, IsAudio, IsVideo
"""
return self.GetMediaType(media_or_hvo) == MediaType.IMAGE
# --- Usage Tracking ---
@OperationsMethod
def GetOwners(self, media_or_hvo):
"""
Get all objects that reference this media file.
Returns objects (entries, pronunciations, examples, etc.) that
have this media file attached.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
list: List of objects referencing this media file (empty if none)
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> owners = project.Media.GetOwners(media)
>>> for owner in owners:
... print(f"Owner: {owner.ClassName} (HVO: {owner.Hvo})")
Owner: LexPronunciation (HVO: 12345)
Owner: LexExampleSentence (HVO: 67890)
Notes:
- Returns all objects with references to this media
- Common owner types: LexPronunciation, LexExampleSentence, etc.
- Empty list if media is not used anywhere
- Owners can be of various types
- Use owner.ClassName to determine type
See Also:
GetOwnerCount
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
# Get all incoming references
# ICmFile objects track their owners via the Owner property
owners = []
if hasattr(media, "Owner") and media.Owner:
owners.append(media.Owner)
return owners
@OperationsMethod
def GetOwnerCount(self, media_or_hvo):
"""
Get the count of objects referencing this media file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
int: Number of objects using this media file
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> count = project.Media.GetOwnerCount(media)
>>> print(f"Used by {count} objects")
Used by 3 objects
>>> # Check if media is orphaned
>>> if project.Media.GetOwnerCount(media) == 0:
... print("This media file is not used anywhere")
Notes:
- Returns 0 if media is not used anywhere
- More efficient than len(GetOwners()) for large projects
- Useful for finding orphaned media files
See Also:
GetOwners
"""
return len(self.GetOwners(media_or_hvo))
# --- Utility Methods ---
@OperationsMethod
def CopyToProject(self, external_path, internal_subdir="AudioVisual", label=None, wsHandle=None):
"""
Copy an external file into the project's LinkedFiles directory
and create a media reference.
Args:
external_path: Path to the external file to import
internal_subdir: Subdirectory within LinkedFiles (default: "AudioVisual")
label: Optional descriptive label for the media file
wsHandle: Optional writing system handle. Defaults to analysis WS.
Returns:
ICmFile: The created media file object
Raises:
FP_ReadOnlyError: If project is not opened with write enabled
FP_NullParameterError: If external_path is None
FP_ParameterError: If external_path is empty or file doesn't exist
Example:
>>> # Import an external audio file
>>> media = project.Media.CopyToProject(
... "/home/user/recordings/audio.wav",
... internal_subdir="AudioVisual",
... label="Speaker 1"
... )
>>> print(project.Media.GetInternalPath(media))
LinkedFiles/AudioVisual/audio.wav
>>> # Import an image
>>> photo = project.Media.CopyToProject(
... "/home/user/photos/item.jpg",
... internal_subdir="Pictures"
... )
Notes:
- Copies the file to project's LinkedFiles directory
- Creates the subdirectory if it doesn't exist
- Preserves the original filename
- If file already exists in destination, appends number
- Creates ICmFile reference with relative path
- Returns None if LinkedFilesRootDir not available
See Also:
Create, SetInternalPath
"""
self._EnsureWriteEnabled()
self._ValidateParam(external_path, "external_path")
if not external_path or not external_path.strip():
raise FP_ParameterError("External path cannot be empty")
external_path = external_path.strip()
if not os.path.exists(external_path):
raise FP_ParameterError(f"File not found: {external_path}")
# Get LinkedFiles directory
if not hasattr(self.project.project, "LinkedFilesRootDir"):
# If LinkedFilesRootDir not available, create reference only
logger.warning("LinkedFilesRootDir not available, creating reference without copying")
return self.Create(external_path, label, wsHandle)
linked_files_dir = self.project.project.LinkedFilesRootDir
target_dir = os.path.join(linked_files_dir, internal_subdir)
# Create target directory if it doesn't exist
if not os.path.exists(target_dir):
os.makedirs(target_dir)
# Get filename and create target path
filename = os.path.basename(external_path)
target_path = os.path.join(target_dir, filename)
# If file exists, append number
if os.path.exists(target_path):
base, ext = os.path.splitext(filename)
counter = 1
while os.path.exists(os.path.join(target_dir, f"{base}_{counter}{ext}")):
counter += 1
filename = f"{base}_{counter}{ext}"
target_path = os.path.join(target_dir, filename)
# Copy the file
shutil.copy2(external_path, target_path)
# Create internal path (relative to LinkedFiles)
internal_path = os.path.join("LinkedFiles", internal_subdir, filename)
# Create media reference
return self.Create(internal_path, label, wsHandle)
@OperationsMethod
def IsValid(self, media_or_hvo):
"""
Check if a media file's referenced file exists on disk.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
bool: True if the file exists on disk, False otherwise
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> if project.Media.IsValid(media):
... print("File exists on disk")
... else:
... print("File is missing!")
>>> # Find all missing media files
>>> for media in project.Media.GetAll():
... if not project.Media.IsValid(media):
... path = project.Media.GetInternalPath(media)
... print(f"Missing: {path}")
Notes:
- Checks if the actual file exists on the file system
- Uses GetExternalPath() to resolve full path
- Returns False if path cannot be resolved
- Returns False if path is empty
- Useful for finding broken media references
See Also:
GetExternalPath, GetInternalPath
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
try:
external_path = self.GetExternalPath(media_or_hvo)
if not external_path:
return False
return os.path.exists(external_path)
except (OSError, FP_ParameterError, FP_NullParameterError):
return False
except Exception as e:
logger.warning(f"Unexpected error checking media file existence: {e}")
return False
@OperationsMethod
def GetGuid(self, media_or_hvo):
"""
Get the GUID (Globally Unique Identifier) of a media file.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
System.Guid: The media file's GUID
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> guid = project.Media.GetGuid(media)
>>> print(guid)
a1b2c3d4-e5f6-7890-abcd-ef1234567890
>>> # Get as string
>>> guid_str = str(guid)
>>> print(guid_str)
a1b2c3d4-e5f6-7890-abcd-ef1234567890
>>> # Use GUID to retrieve media later
>>> media2 = project.Object(guid)
>>> print(project.Media.GetInternalPath(media2))
audio.wav
Notes:
- GUIDs are unique across all FLEx projects
- GUIDs are persistent (don't change)
- Useful for linking media across projects
- Can be used with FLExProject.Object() to retrieve media
See Also:
FLExProject.Object
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
# Resolve to media object
if isinstance(media_or_hvo, int):
media = self.project.Object(media_or_hvo)
if not isinstance(media, ICmFile):
raise FP_ParameterError("HVO does not refer to a media file")
else:
media = media_or_hvo
return media.Guid
@OperationsMethod
def GetHvo(self, media):
"""
Get the HVO (database ID) of a media file.
Args:
media: An ICmFile object
Returns:
int: The media file's HVO
Raises:
FP_NullParameterError: If media is None
Example:
>>> media = project.Media.Find("audio.wav")
>>> hvo = project.Media.GetHvo(media)
>>> print(hvo)
12345
>>> # Use HVO to retrieve media later
>>> media2 = project.Object(hvo)
>>> print(project.Media.GetInternalPath(media2))
audio.wav
Notes:
- HVOs are database IDs (integers)
- HVOs are unique within a project
- HVOs may change if database is rebuilt
- Prefer GUIDs for persistent references
See Also:
GetGuid, FLExProject.Object
"""
self._ValidateParam(media, "media")
return media.Hvo
@OperationsMethod
def GetFileSize(self, media_or_hvo):
"""
Get the file size of a media file in bytes.
Args:
media_or_hvo: Either an ICmFile object or its HVO
Returns:
int: File size in bytes, or -1 if file doesn't exist
Raises:
FP_NullParameterError: If media_or_hvo is None
FP_ParameterError: If media doesn't exist
Example:
>>> media = project.Media.Find("audio.wav")
>>> size = project.Media.GetFileSize(media)
>>> print(f"File size: {size / 1024:.2f} KB")
File size: 1234.56 KB
>>> # Get size in human-readable format
>>> if size > 1024 * 1024:
... print(f"{size / (1024 * 1024):.2f} MB")
... elif size > 1024:
... print(f"{size / 1024:.2f} KB")
... else:
... print(f"{size} bytes")
Notes:
- Returns -1 if file doesn't exist on disk
- Uses GetExternalPath() to resolve file location
- Size is in bytes
- Returns actual file size from file system
See Also:
IsValid, GetExternalPath
"""
self._ValidateParam(media_or_hvo, "media_or_hvo")
try:
external_path = self.GetExternalPath(media_or_hvo)
if not external_path or not os.path.exists(external_path):
return -1
return os.path.getsize(external_path)
except (OSError, FP_ParameterError, FP_NullParameterError):
return -1
except Exception as e:
logger.warning(f"Unexpected error getting media file size: {e}")
return -1
@OperationsMethod
def GetAllByType(self, media_type):
"""
Get all media files of a specific type.
Args:
media_type: Media type constant (MediaType.AUDIO, VIDEO, IMAGE)
Yields:
ICmFile: Each media file of the specified type
Raises:
FP_ParameterError: If media_type is invalid
Example:
>>> # Get all audio files
>>> for media in project.Media.GetAllByType(MediaType.AUDIO):
... path = project.Media.GetInternalPath(media)
... print(f"Audio: {path}")
Audio: audio1.wav
Audio: audio2.mp3
>>> # Count videos
>>> video_count = sum(1 for _ in project.Media.GetAllByType(MediaType.VIDEO))
>>> print(f"Total videos: {video_count}")
Notes:
- Returns an iterator for memory efficiency
- Valid types: MediaType.AUDIO, VIDEO, IMAGE, UNKNOWN
- Uses GetMediaType() for detection
- Detection is based on file extension
See Also:
GetAll, GetMediaType, IsAudio, IsVideo, IsImage
"""
if media_type not in (MediaType.AUDIO, MediaType.VIDEO, MediaType.IMAGE, MediaType.UNKNOWN):
raise FP_ParameterError(
f"Invalid media type: {media_type}. " f"Use MediaType.AUDIO, VIDEO, IMAGE, or UNKNOWN"
)
for media in self.GetAll():
if self.GetMediaType(media) == media_type:
yield media
@OperationsMethod
def GetOrphanedMedia(self):
"""
Get all media files that are not referenced by any object.
Yields:
ICmFile: Each orphaned media file
Example:
>>> # Find orphaned media files
>>> orphaned = list(project.Media.GetOrphanedMedia())
>>> print(f"Found {len(orphaned)} orphaned media files")
Found 5 orphaned media files
>>> # Delete orphaned media
>>> for media in project.Media.GetOrphanedMedia():
... path = project.Media.GetInternalPath(media)
... print(f"Deleting orphaned: {path}")
... project.Media.Delete(media)
Notes:
- Returns an iterator for memory efficiency
- Orphaned media has no owners (GetOwnerCount() == 0)
- Useful for cleaning up unused media
- Consider backing up before deleting orphaned media
See Also:
GetOwnerCount, GetOwners, Delete
"""
for media in self.GetAll():
if self.GetOwnerCount(media) == 0:
yield media