Skip to main content

flet-media-library

A high-performance Flet service extension for querying, displaying, saving, mutating, and monitoring the device media library from Python on Android and iOS.

The Python API is Flet-native, powered on Flutter by photo_manager with dedicated native Android Kotlin extensions for saving audio, renaming files, and scoped-storage relative folder moving.

PyPI License: MIT Platform Flet


Table of Contents


Key Features

  • Purpose-Specific Media Access: Compliant with Google Play and Apple App Store policies. Never asks for MANAGE_EXTERNAL_STORAGE.
  • Broad Media Format Support: Handles photos, videos, and audio files across Android and iOS.
  • Fast Base64 Thumbnails: Efficiently generates thumbnails for both pictures and video frames directly into Flet ft.Image(src=...).
  • Album & Bucket Browsing: Fetch standard and custom user albums (Camera, Screenshots, Download, Music, WhatsApp, etc.).
  • Rich Filtering & Sorting: Sort by date added, date modified, size, duration, or filename, with pagination (limit, offset, has_more).
  • In-Place Gallery Mutations: Delete single or batch assets, rename files, copy assets between albums, and move assets across directories.
  • Live Change Events: Subscribe to real-time additions, deletions, or edits in the device media store.
  • Structured Error Handling: Dedicated typed exceptions (PermissionRequiredError, UnsupportedError, AssetNotFoundError, etc.).

Permissions Philosophy (No All-Files Access)

flet-media-library strictly follows the principle of least privilege. You do not need (and should never request) broad filesystem access or MANAGE_EXTERNAL_STORAGE for media operations.

Permission Matrix

Android API Version Permission Mechanism Purpose
Android 6 – 12 (API 23–32) READ_EXTERNAL_STORAGE (maxSdkVersion=32) Reads shared media library
Android 13+ (API 33+) READ_MEDIA_IMAGES
READ_MEDIA_VIDEO
READ_MEDIA_AUDIO
Granular permission for specific media types
Android 14+ (API 34+) READ_MEDIA_VISUAL_USER_SELECTED User grants access only to selected items
iOS 14+ PhotoKit Authorization Full, limited (partial), or denied access
Saving Media MediaStore / PhotoKit Insert APIs No broad write permission needed on modern systems

Android Manifest Declarations

This package automatically bundles the following permissions into your Android build manifest:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

iOS Info.plist Keys

When compiling for iOS (flet build ipa), declare usage descriptions for the photo library:

<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to your photo library to browse and select media.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app requires permission to save photos and videos to your gallery.</string>

Installation & Quickstart

PyPI

pip install flet-media-library
# or using uv
uv add flet-media-library

Add to pyproject.toml

[project]
dependencies = [
    "flet>=1.0.0",
    "flet-media-library>=1.0.0",
]

Quickstart Example

import flet as ft
from flet_media_library import MediaLibrary

async def main(page: ft.Page):
    media = MediaLibrary()
    page.services.append(media)
    page.update()

    # Request permissions for images and videos
    status = await media.request_permissions(["image", "video"])
    if not status.all_granted and not status.any_limited:
        page.add(ft.Text("Media permission was denied!"))
        return

    # Query latest 20 photos
    page_data = await media.get_assets(media_type="image", limit=20)
    grid = ft.GridView(expand=True, max_extent=120, spacing=5, run_spacing=5)
    
    for asset in page_data.items:
        # Load thumbnail as base64
        thumb_b64 = await media.get_thumbnail(asset.id, width=120, height=120)
        grid.controls.append(
            ft.Image(src_base64=thumb_b64, fit=ft.BoxFit.COVER, border_radius=4)
        )

    page.add(grid)

if __name__ == "__main__":
    ft.run(main)

Comprehensive API Reference

Service Registration

MediaLibrary inherits from ft.Service. It must be added to page.services:

from flet_media_library import MediaLibrary

media = MediaLibrary()
page.services.append(media)
page.update()

Permissions APIs

check_permissions(media_types: list[str] | None = None) -> MediaPermissionStatus

Checks the current permission status without prompting the user.

  • Parameters: media_types — List of types to check: ["image", "video", "audio"] (defaults to all).
  • Returns: MediaPermissionStatus object with per-type status.
status = await media.check_permissions(["image", "video"])
print("Images status:", status["image"])  # "granted", "denied", "limited", etc.

request_permissions(media_types: list[str] | None = None) -> MediaPermissionStatus

Prompts the OS system permission dialog requesting access for the specified media types.

  • Parameters: media_types — ["image", "video", "audio"] (request only what you need).
  • Returns: MediaPermissionStatus.
status = await media.request_permissions(["image", "video", "audio"])
if status.all_granted:
    print("Full media library access granted")
elif status.any_limited:
    print("User granted limited/partial media access")

present_limited(media_types: list[str] | None = None) -> None

Re-opens the system limited-picker on iOS 14+ or Android 14+ so the user can select additional photos/videos without having to grant full library access.

await media.present_limited(["image", "video"])

open_settings() -> None

Navigates the user directly to the application's system settings screen (useful when permission is denied_forever).

await media.open_settings()

Album Queries

get_albums(media_type: str = "all") -> list[MediaAlbum]

Fetches albums/folders containing the specified media type.

  • Parameters: media_type — "all", "image", "video", or "audio".
  • Returns: list[MediaAlbum].
albums = await media.get_albums(media_type="image")
for album in albums:
    print(f"Album: {album.name} (ID: {album.id}) - {album.asset_count} items")

Asset Queries & Pagination

get_assets(...) -> MediaAssetPage

Queries assets with pagination, optional album filtering, and sorting.

async def get_assets(
    media_type: str = "all",           # "all" | "image" | "video" | "audio"
    album: str | None = None,           # Album ID from get_albums(), or None for root
    mime_type: str | None = None,       # Exact MIME filter (e.g. "video/mp4", Android only)
    limit: int = 50,                    # 1 to 500 items per page
    offset: int = 0,                    # Item offset to skip
    sort_by: str = "date_added",        # "date_added", "date_modified", "display_name", "size", "duration"
    sort_order: str = "desc",           # "desc" | "asc"
) -> MediaAssetPage
# Query page 1 (first 30 videos sorted newest first)
page1 = await media.get_assets(
    media_type="video",
    limit=30,
    offset=0,
    sort_by="date_added",
    sort_order="desc",
)

print(f"Total: {page1.total}, Loaded: {len(page1.items)}, Has more: {page1.has_more}")

# Query next page
if page1.has_more:
    page2 = await media.get_assets(media_type="video", limit=30, offset=30)

get_asset(asset_id: str) -> MediaAsset

Retrieves detailed metadata for a single specific asset by its ID.

asset = await media.get_asset("1000000032")
print(f"{asset.display_name} - {asset.width}x{asset.height} - {asset.size} bytes")

Thumbnails

get_thumbnail(asset_id: str, width: int = 200, height: int = 200, quality: int = 90) -> str

Generates a base64-encoded JPEG thumbnail for an image or a video frame.

  • Parameters:
    • asset_id: ID of the asset.
    • width: Desired thumbnail width in pixels.
    • height: Desired thumbnail height in pixels.
    • quality: JPEG compression quality (1–100).
  • Returns: Base64 string suitable for ft.Image(src_base64=...).
thumb_b64 = await media.get_thumbnail(asset.id, width=150, height=150, quality=85)
image_ctrl = ft.Image(src_base64=thumb_b64, width=150, height=150)

Saving Media (Images, Videos, Audio)

Saves local files directly into the platform media gallery using platform MediaStore / PhotoKit insert pipelines.

save_image(file_path: str, file_name: str | None = None, album: str | None = None) -> MediaAsset

Saves an image file into the device gallery.

  • file_path: Absolute path to source image.
  • file_name: Optional target filename (e.g. "my_snapshot.jpg").
  • album: Optional album folder name (e.g. "Pictures/MyApp").
asset = await media.save_image(
    "/data/user/0/com.app/cache/photo.jpg",
    file_name="snapshot_2026.jpg",
    album="Pictures/MyCameraApp",
)
print("Saved image ID:", asset.id)

save_video(file_path: str, file_name: str | None = None, album: str | None = None) -> MediaAsset

Saves a video file into the device gallery.

asset = await media.save_video(
    "/path/to/recording.mp4",
    file_name="clip.mp4",
    album="Movies/MyCameraApp",
)

save_audio(file_path: str, file_name: str | None = None, album: str | None = None) -> MediaAsset

Saves an audio file into the device gallery (Android only; raises UnsupportedError on iOS).

asset = await media.save_audio(
    "/path/to/voice_note.m4a",
    file_name="recording_01.m4a",
    album="Music/Recordings",
)

Mutations (Rename, Move, Copy, Delete)

delete_asset(asset_id: str) -> bool

Deletes a single asset.

  • Returns: True if successfully deleted, False if user cancelled system dialog.
ok = await media.delete_asset(asset.id)
if ok:
    print("Asset deleted successfully")

delete_assets(asset_ids: list[str]) -> list[str]

Batch deletes multiple assets.

  • Returns: List of asset IDs that were successfully deleted.
deleted_ids = await media.delete_assets(["id1", "id2", "id3"])
print(f"Deleted {len(deleted_ids)} items")

rename_asset(asset_id: str, new_name: str) -> bool

Renames an asset's display name, including its extension (Android only). On Android 11+, the system may present a user confirmation dialog.

ok = await media.rename_asset(asset.id, "vacation_sunset.jpg")

move_asset(asset_id: str, target_relative_path: str) -> bool

Moves an asset to another directory by relative path (Android 10+ only, e.g., "Pictures/Archive").

ok = await media.move_asset(asset.id, "Pictures/Archive")
if ok:
    print("Moved to archive folder")

copy_asset(asset_id: str, target_album: str) -> MediaAsset

Copies an asset into a target album. Platform support varies:

  • Android < 11: Creates duplicate file.
  • Android 11+: Scoped storage restricts arbitrary duplication; raises UnsupportedError.
  • iOS: Links asset to the target album.

Live Media Change Notifications

Subscribe to system media library changes (such as when new photos are taken by the camera or downloaded).

def on_media_changed(e: ft.ControlEvent):
    event = e.data  # MediaChangeEvent
    print(f"Change detected: type={event.change_type}, id={event.asset_id}")

media.on_change = on_media_changed
await media.start_change_notify()

# When finished or exiting screen:
await media.stop_change_notify()

Cache Management

clear_file_cache() -> None

Clears internal thumbnail and file caches cached by the underlying plugin.

await media.clear_file_cache()

Data Models

MediaAsset

Represents a single media item:

Field Type Description
id str Platform-unique asset identifier
display_name str File name including extension (e.g. IMG_001.jpg)
mime_type str MIME type (e.g. image/jpeg, video/mp4, audio/mp4)
media_type str Broad type: "image", "video", or "audio"
size int Size in bytes
width int Pixel width (0 for audio)
height int Pixel height (0 for audio)
duration_ms int Duration in milliseconds (0 for images)
date_added int Unix timestamp (seconds) when added to library
date_modified int Unix timestamp (seconds) when last modified
orientation int EXIF orientation angle (0, 90, 180, 270)
album_id str Containing album identifier
album_name str Containing album name
relative_path str Relative directory (e.g. DCIM/Camera/)
source_uri str System URI (content://... on Android, ph://... on iOS)

MediaAlbum

Represents an album, bucket, or folder:

Field Type Description
id str Album unique identifier
name str User-facing album title (e.g. Camera, Screenshots)
asset_count int Count of media items in this album
media_types list[str] Media types present in album (["image", "video"])
is_all bool True if this is the "Recent" / "All Media" collection
is_system_album bool True for OS-managed collections
platform_identifier str Native platform identifier

MediaPermissionStatus

Snapshot of permission states:

Property / Method Type Description
states dict[str, str] Maps media type to status (granted, limited, denied, denied_forever, restricted, unknown)
status[media_type] str Shortcut for accessing status by key (e.g. status["image"])
all_granted bool True if all requested types are "granted"
any_limited bool True if any requested type is "limited"
can_request bool True if system dialog can still be requested

MediaAssetPage

Result of a paginated get_assets query:

Field Type Description
items list[MediaAsset] List of MediaAsset items for this page
total int Total count matching filter
offset int Current query offset
limit int Items per page
has_more bool True if more items are available

Exception Hierarchy

All exceptions inherit from MediaLibraryError:

MediaLibraryError
 ├── PermissionRequiredError   # Operation attempted before permissions were granted
 ├── PermissionDeniedError     # User actively denied permission
 ├── UnsupportedError          # Method unsupported on this OS or OS version
 ├── AssetNotFoundError        # Target asset ID does not exist
 ├── AlbumNotFoundError        # Target album ID does not exist
 ├── InvalidArgumentError      # Bad arguments (e.g. limit > 500, negative offset)
 └── PlatformError             # Underlying native platform exception

Practical Cookbooks & Examples

Cookbook 1: Request Permissions & Load Thumbnail Grid

import flet as ft
from flet_media_library import MediaLibrary, PermissionRequiredError

async def main(page: ft.Page):
    media = MediaLibrary()
    page.services.append(media)
    page.update()

    status = await media.request_permissions(["image", "video"])
    if not (status.all_granted or status.any_limited):
        page.add(ft.Text("Permissions denied. Cannot browse media."))
        return

    grid = ft.GridView(expand=True, max_extent=110, spacing=4, run_spacing=4)
    page.add(grid)

    asset_page = await media.get_assets(media_type="image", limit=40)
    for asset in asset_page.items:
        thumb = await media.get_thumbnail(asset.id, width=110, height=110)
        grid.controls.append(
            ft.Image(src_base64=thumb, fit=ft.BoxFit.COVER, border_radius=4)
        )
    page.update()

ft.run(main)
import tempfile
from pathlib import Path
import flet as ft
from flet_media_library import MediaLibrary

async def save_recording(media: MediaLibrary, recorded_temp_file: str):
    # Save audio file to Android Music/VoiceNotes
    asset = await media.save_audio(
        recorded_temp_file,
        file_name="voice_note.m4a",
        album="Music/VoiceNotes",
    )
    print(f"Saved into gallery with ID: {asset.id}")

Cookbook 3: Moving and Renaming Assets (Android)

from flet_media_library import MediaLibrary, UnsupportedError

async def organize_media(media: MediaLibrary, asset_id: str):
    try:
        # Rename file
        renamed = await media.rename_asset(asset_id, "family_vacation_2026.jpg")
        print("Renamed:", renamed)

        # Move to Archive folder
        moved = await media.move_asset(asset_id, "Pictures/Archive")
        print("Moved:", moved)
    except UnsupportedError as err:
        print("Operation not supported on this platform/OS version:", err)

Cookbook 4: Real-time Change Monitoring

async def watch_gallery(page: ft.Page, media: MediaLibrary):
    async def on_change(e):
        ev = e.data
        print(f"Library updated! Type: {ev.change_type}, Asset: {ev.asset_id}")
        # Refresh UI
        page.snack_bar = ft.SnackBar(ft.Text("Media gallery changed!"))
        page.snack_bar.open = True
        page.update()

    media.on_change = on_change
    await media.start_change_notify()

Building Packaged Mobile Apps

When packaging with Flet, the Flutter engine plugin is automatically linked into the mobile binary.

Android (APK)

uv run flet build apk \
  --split-per-abi \
  --arch arm64-v8a \
  --permissions camera microphone \
  --yes

To build against a local development version of this repository:

uv run flet build apk \
  --source-packages /path/to/flet_media_library/src/flutter/flet_media_library \
  --permissions camera microphone \
  --yes

iOS (IPA / Simulator)

# Simulator build
uv run flet build ios-simulator --permissions camera microphone --yes

# Production IPA
uv run flet build ipa \
  --permissions camera microphone \
  --info-plist NSPhotoLibraryUsageDescription="Browse and select media from device gallery" \
  --info-plist NSPhotoLibraryAddUsageDescription="Save photos and captured media to library"

Local Development Setup

To run and contribute to flet-media-library:

git clone https://github.com/fazi-gondal/Flet-media-library.git
cd Flet-media-library

# Install Python package in editable mode
uv sync

# Test Python models & serialization
uv run pytest tests/

# Format Dart plugin code
cd src/flutter/flet_media_library
flutter pub get
flutter analyze
dart format --set-exit-if-changed --output=none .

To explore and test on a physical device or emulator, see the full-featured test harness in examples/media_library_demo/.

Release files for flet-media-library 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flet-media-library 1.0.0
File Size Uploaded
flet_media_library-1.0.0.tar.gz 40.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flet-media-library 1.0.0
File Interpreter ABI Platform
flet_media_library-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 76.7 kB

Release files / flet_media_library-1.0.0.tar.gz

Download URL flet_media_library-1.0.0.tar.gz
Size 40.8 kB
Tags Source
SHA-256 checksum
How to use checksums
cdcb659687286c8b28176f3a66d5d5d7a1241b17dd870cfe56991391411f1fdd
BLAKE2b-256 checksum
How to use checksums
5092e46ac17a96f8544c4161d2ea89a17b981cecd8dad86178524a6696494646
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / flet_media_library-1.0.0-py3-none-any.whl

Download URL flet_media_library-1.0.0-py3-none-any.whl
Size 35.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6256e1eccb5937c08103e8338546a07072772ac60f0e94ff56e720d42557eafa
BLAKE2b-256 checksum
How to use checksums
56b7bdee4cc6b937b60231aa041966d3ac77dc4b2f407e7c99d7c2480dbab624
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

1.1.2

2 release files

1.0.2

2 release files

1.0.1

2 release files

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page