flet-media-scanner 0.86.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flet_media_scanner-0.86.0/PKG-INFO +125 -0
- flet_media_scanner-0.86.0/README.md +104 -0
- flet_media_scanner-0.86.0/pyproject.toml +39 -0
- flet_media_scanner-0.86.0/setup.cfg +4 -0
- flet_media_scanner-0.86.0/src/flet_media_scanner/__init__.py +3 -0
- flet_media_scanner-0.86.0/src/flet_media_scanner/media_scanner.py +124 -0
- flet_media_scanner-0.86.0/src/flet_media_scanner.egg-info/PKG-INFO +125 -0
- flet_media_scanner-0.86.0/src/flet_media_scanner.egg-info/SOURCES.txt +16 -0
- flet_media_scanner-0.86.0/src/flet_media_scanner.egg-info/dependency_links.txt +1 -0
- flet_media_scanner-0.86.0/src/flet_media_scanner.egg-info/requires.txt +1 -0
- flet_media_scanner-0.86.0/src/flet_media_scanner.egg-info/top_level.txt +2 -0
- flet_media_scanner-0.86.0/src/flutter/flet_media_scanner/android/build.gradle +46 -0
- flet_media_scanner-0.86.0/src/flutter/flet_media_scanner/android/src/main/AndroidManifest.xml +2 -0
- flet_media_scanner-0.86.0/src/flutter/flet_media_scanner/android/src/main/kotlin/com/example/flet_media_scanner/FletMediaScannerPlugin.kt +252 -0
- flet_media_scanner-0.86.0/src/flutter/flet_media_scanner/lib/flet_media_scanner.dart +2 -0
- flet_media_scanner-0.86.0/src/flutter/flet_media_scanner/lib/src/extension.dart +17 -0
- flet_media_scanner-0.86.0/src/flutter/flet_media_scanner/lib/src/media_scanner_service.dart +186 -0
- flet_media_scanner-0.86.0/src/flutter/flet_media_scanner/pubspec.yaml +21 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flet-media-scanner
|
|
3
|
+
Version: 0.86.0
|
|
4
|
+
Summary: Flet extension for Android MediaStore — scan, save, list, and delete videos in the Gallery
|
|
5
|
+
Author-email: Fazi-Gondal <nextinpk@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fazi-gondal/Vidsaver
|
|
8
|
+
Project-URL: Repository, https://github.com/fazi-gondal/Vidsaver
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/fazi-gondal/Vidsaver/issues
|
|
10
|
+
Keywords: flet,android,media,mediascanner,gallery,flutter
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: flet>=0.86.0
|
|
21
|
+
|
|
22
|
+
# flet-media-scanner
|
|
23
|
+
|
|
24
|
+
A [Flet](https://flet.dev) extension for Android **MediaStore** integration — save, list, and delete videos in the device Gallery without requiring broad storage permissions.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- **Platform**: Android only (no-op on other platforms)
|
|
29
|
+
- **Android**: API 23+ (minSdk 23)
|
|
30
|
+
- **Flet**: >= 0.86.0
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install flet-media-scanner
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
Register the extension in your `pyproject.toml`:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[tool.flet.extensions]
|
|
44
|
+
flet_media_scanner = "flet_media_scanner.Extension"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import flet as ft
|
|
51
|
+
from flet_media_scanner import MediaScanner, SaveResult
|
|
52
|
+
|
|
53
|
+
async def main(page: ft.Page):
|
|
54
|
+
scanner = MediaScanner()
|
|
55
|
+
page.services.append(scanner)
|
|
56
|
+
page.update()
|
|
57
|
+
|
|
58
|
+
# Save a video to the Gallery (Movies/MyApp folder)
|
|
59
|
+
result: SaveResult = await scanner.save_video(
|
|
60
|
+
"/data/user/0/com.example.app/cache/video.mp4",
|
|
61
|
+
file_name="my_video.mp4",
|
|
62
|
+
album="MyApp",
|
|
63
|
+
)
|
|
64
|
+
if result.success:
|
|
65
|
+
print(f"Saved: {result.content_uri}")
|
|
66
|
+
else:
|
|
67
|
+
print(f"Error: {result.error}")
|
|
68
|
+
|
|
69
|
+
# List videos in the album
|
|
70
|
+
videos = await scanner.list_videos(album="MyApp")
|
|
71
|
+
for v in videos:
|
|
72
|
+
print(v["display_name"], v["content_uri"])
|
|
73
|
+
|
|
74
|
+
# Delete a video by content URI
|
|
75
|
+
deleted = await scanner.delete_video("content://media/external/video/media/123")
|
|
76
|
+
print("Deleted:", deleted)
|
|
77
|
+
|
|
78
|
+
ft.run(main)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API
|
|
82
|
+
|
|
83
|
+
### `MediaScanner` (Service)
|
|
84
|
+
|
|
85
|
+
Add to `page.services` before calling any methods.
|
|
86
|
+
|
|
87
|
+
#### `await save_video(file_path, file_name=None, album="MyApp") → SaveResult`
|
|
88
|
+
|
|
89
|
+
Copies a private app file into Android MediaStore (`Movies/<album>`). Works on Android 10+ without broad storage permissions.
|
|
90
|
+
|
|
91
|
+
| Parameter | Type | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `file_path` | `str` | Absolute path to the source video file |
|
|
94
|
+
| `file_name` | `str \| None` | Display name in Gallery (defaults to basename) |
|
|
95
|
+
| `album` | `str` | Subfolder inside `Movies/` |
|
|
96
|
+
|
|
97
|
+
#### `await list_videos(album="MyApp") → list[dict]`
|
|
98
|
+
|
|
99
|
+
Returns all videos previously saved to the album. Each dict contains: `display_name`, `content_uri`, `mime_type`, `relative_path`, `size`, `date_added`.
|
|
100
|
+
|
|
101
|
+
#### `await delete_video(content_uri) → bool`
|
|
102
|
+
|
|
103
|
+
Deletes a MediaStore item by its `content://` URI.
|
|
104
|
+
|
|
105
|
+
#### `await scan_media(file_path) → bool`
|
|
106
|
+
|
|
107
|
+
*(Legacy)* Triggers a media scan for a file that already exists in public storage. Use `save_video()` for new downloads instead.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `SaveResult` (dataclass)
|
|
112
|
+
|
|
113
|
+
| Field | Type | Description |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `success` | `bool` | Whether the operation succeeded |
|
|
116
|
+
| `content_uri` | `str` | MediaStore `content://` URI |
|
|
117
|
+
| `display_name` | `str` | File name as shown in Gallery |
|
|
118
|
+
| `mime_type` | `str` | e.g. `video/mp4` |
|
|
119
|
+
| `relative_path` | `str` | e.g. `Movies/MyApp/` |
|
|
120
|
+
| `size` | `int` | File size in bytes |
|
|
121
|
+
| `error` | `str` | Error message if `success=False` |
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# flet-media-scanner
|
|
2
|
+
|
|
3
|
+
A [Flet](https://flet.dev) extension for Android **MediaStore** integration — save, list, and delete videos in the device Gallery without requiring broad storage permissions.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **Platform**: Android only (no-op on other platforms)
|
|
8
|
+
- **Android**: API 23+ (minSdk 23)
|
|
9
|
+
- **Flet**: >= 0.86.0
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install flet-media-scanner
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Register the extension in your `pyproject.toml`:
|
|
20
|
+
|
|
21
|
+
```toml
|
|
22
|
+
[tool.flet.extensions]
|
|
23
|
+
flet_media_scanner = "flet_media_scanner.Extension"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import flet as ft
|
|
30
|
+
from flet_media_scanner import MediaScanner, SaveResult
|
|
31
|
+
|
|
32
|
+
async def main(page: ft.Page):
|
|
33
|
+
scanner = MediaScanner()
|
|
34
|
+
page.services.append(scanner)
|
|
35
|
+
page.update()
|
|
36
|
+
|
|
37
|
+
# Save a video to the Gallery (Movies/MyApp folder)
|
|
38
|
+
result: SaveResult = await scanner.save_video(
|
|
39
|
+
"/data/user/0/com.example.app/cache/video.mp4",
|
|
40
|
+
file_name="my_video.mp4",
|
|
41
|
+
album="MyApp",
|
|
42
|
+
)
|
|
43
|
+
if result.success:
|
|
44
|
+
print(f"Saved: {result.content_uri}")
|
|
45
|
+
else:
|
|
46
|
+
print(f"Error: {result.error}")
|
|
47
|
+
|
|
48
|
+
# List videos in the album
|
|
49
|
+
videos = await scanner.list_videos(album="MyApp")
|
|
50
|
+
for v in videos:
|
|
51
|
+
print(v["display_name"], v["content_uri"])
|
|
52
|
+
|
|
53
|
+
# Delete a video by content URI
|
|
54
|
+
deleted = await scanner.delete_video("content://media/external/video/media/123")
|
|
55
|
+
print("Deleted:", deleted)
|
|
56
|
+
|
|
57
|
+
ft.run(main)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### `MediaScanner` (Service)
|
|
63
|
+
|
|
64
|
+
Add to `page.services` before calling any methods.
|
|
65
|
+
|
|
66
|
+
#### `await save_video(file_path, file_name=None, album="MyApp") → SaveResult`
|
|
67
|
+
|
|
68
|
+
Copies a private app file into Android MediaStore (`Movies/<album>`). Works on Android 10+ without broad storage permissions.
|
|
69
|
+
|
|
70
|
+
| Parameter | Type | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `file_path` | `str` | Absolute path to the source video file |
|
|
73
|
+
| `file_name` | `str \| None` | Display name in Gallery (defaults to basename) |
|
|
74
|
+
| `album` | `str` | Subfolder inside `Movies/` |
|
|
75
|
+
|
|
76
|
+
#### `await list_videos(album="MyApp") → list[dict]`
|
|
77
|
+
|
|
78
|
+
Returns all videos previously saved to the album. Each dict contains: `display_name`, `content_uri`, `mime_type`, `relative_path`, `size`, `date_added`.
|
|
79
|
+
|
|
80
|
+
#### `await delete_video(content_uri) → bool`
|
|
81
|
+
|
|
82
|
+
Deletes a MediaStore item by its `content://` URI.
|
|
83
|
+
|
|
84
|
+
#### `await scan_media(file_path) → bool`
|
|
85
|
+
|
|
86
|
+
*(Legacy)* Triggers a media scan for a file that already exists in public storage. Use `save_video()` for new downloads instead.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### `SaveResult` (dataclass)
|
|
91
|
+
|
|
92
|
+
| Field | Type | Description |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `success` | `bool` | Whether the operation succeeded |
|
|
95
|
+
| `content_uri` | `str` | MediaStore `content://` URI |
|
|
96
|
+
| `display_name` | `str` | File name as shown in Gallery |
|
|
97
|
+
| `mime_type` | `str` | e.g. `video/mp4` |
|
|
98
|
+
| `relative_path` | `str` | e.g. `Movies/MyApp/` |
|
|
99
|
+
| `size` | `int` | File size in bytes |
|
|
100
|
+
| `error` | `str` | Error message if `success=False` |
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flet-media-scanner"
|
|
3
|
+
version = "0.86.0"
|
|
4
|
+
description = "Flet extension for Android MediaStore — scan, save, list, and delete videos in the Gallery"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Fazi-Gondal", email = "nextinpk@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
keywords = ["flet", "android", "media", "mediascanner", "gallery", "flutter"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"flet>=0.86.0"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/fazi-gondal/Vidsaver"
|
|
27
|
+
Repository = "https://github.com/fazi-gondal/Vidsaver"
|
|
28
|
+
"Bug Tracker" = "https://github.com/fazi-gondal/Vidsaver/issues"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["setuptools"]
|
|
32
|
+
build-backend = "setuptools.build_meta"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = {"" = "src"}
|
|
36
|
+
packages = ["flet_media_scanner", "flutter.flet_media_scanner"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
"flutter.flet_media_scanner" = ["**/*"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import flet as ft
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SaveResult:
|
|
11
|
+
success: bool = False
|
|
12
|
+
content_uri: str = ""
|
|
13
|
+
display_name: str = ""
|
|
14
|
+
mime_type: str = ""
|
|
15
|
+
relative_path: str = ""
|
|
16
|
+
source_path: str = ""
|
|
17
|
+
size: int = 0
|
|
18
|
+
error: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@ft.control("MediaScanner")
|
|
22
|
+
class MediaScanner(ft.Service):
|
|
23
|
+
"""
|
|
24
|
+
Android media service for publishing videos through MediaStore.
|
|
25
|
+
|
|
26
|
+
New downloads should use save_video(). scan_media() is kept only for
|
|
27
|
+
legacy files that already exist in public storage and need Gallery indexing.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
on_saved: Optional[ft.EventHandler[Any]] = None
|
|
31
|
+
on_scanned: Optional[ft.EventHandler[Any]] = None
|
|
32
|
+
|
|
33
|
+
async def save_video(
|
|
34
|
+
self,
|
|
35
|
+
file_path: str,
|
|
36
|
+
file_name: str | None = None,
|
|
37
|
+
album: str = "Vidsaver",
|
|
38
|
+
) -> SaveResult:
|
|
39
|
+
"""
|
|
40
|
+
Copy an app-private video into Android MediaStore.Video.
|
|
41
|
+
|
|
42
|
+
On Android 10+ this publishes to Movies/<album> without requiring
|
|
43
|
+
broad storage permissions or a media scan.
|
|
44
|
+
"""
|
|
45
|
+
if not file_path or not os.path.exists(file_path):
|
|
46
|
+
return SaveResult(error=f"file does not exist: {file_path}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
result = await self._invoke_method(
|
|
50
|
+
"save_video",
|
|
51
|
+
{
|
|
52
|
+
"path": file_path,
|
|
53
|
+
"file_name": file_name or os.path.basename(file_path),
|
|
54
|
+
"album": album,
|
|
55
|
+
},
|
|
56
|
+
timeout=60.0,
|
|
57
|
+
)
|
|
58
|
+
payload = json.loads(result) if result else {}
|
|
59
|
+
return SaveResult(
|
|
60
|
+
success=bool(payload.get("success")),
|
|
61
|
+
content_uri=str(payload.get("content_uri") or ""),
|
|
62
|
+
display_name=str(payload.get("display_name") or ""),
|
|
63
|
+
mime_type=str(payload.get("mime_type") or ""),
|
|
64
|
+
relative_path=str(payload.get("relative_path") or ""),
|
|
65
|
+
source_path=str(payload.get("source_path") or file_path),
|
|
66
|
+
size=int(payload.get("size") or 0),
|
|
67
|
+
error=str(payload.get("error") or ""),
|
|
68
|
+
)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return SaveResult(error=str(e), source_path=file_path)
|
|
71
|
+
|
|
72
|
+
async def scan_media(self, file_path: str) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Scan a legacy public media file so it appears in Gallery/Photos.
|
|
75
|
+
|
|
76
|
+
New downloads should use save_video() instead; MediaStore inserts do
|
|
77
|
+
not need this scan.
|
|
78
|
+
"""
|
|
79
|
+
if not file_path or not os.path.exists(file_path):
|
|
80
|
+
print(f"[MediaScanner] scan_media: file does not exist: {file_path}")
|
|
81
|
+
return False
|
|
82
|
+
try:
|
|
83
|
+
result = await self._invoke_method(
|
|
84
|
+
"scan_media",
|
|
85
|
+
{"path": file_path},
|
|
86
|
+
timeout=15.0,
|
|
87
|
+
)
|
|
88
|
+
success = result == "true"
|
|
89
|
+
print(f"[MediaScanner] scan_media: {file_path} -> result={result} success={success}")
|
|
90
|
+
return success
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"[MediaScanner] scan_media error: {file_path}: {e}")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
async def delete_video(self, content_uri: str) -> bool:
|
|
96
|
+
"""Delete a MediaStore item by its content URI."""
|
|
97
|
+
if not content_uri:
|
|
98
|
+
return False
|
|
99
|
+
try:
|
|
100
|
+
result = await self._invoke_method(
|
|
101
|
+
"delete_video",
|
|
102
|
+
{"content_uri": content_uri},
|
|
103
|
+
timeout=15.0,
|
|
104
|
+
)
|
|
105
|
+
payload = json.loads(result) if result else {}
|
|
106
|
+
return bool(payload.get("success"))
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"[MediaScanner] delete_video error: {content_uri}: {e}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
async def list_videos(self, album: str = "Vidsaver") -> list[dict]:
|
|
112
|
+
"""List videos previously saved to the MediaStore album."""
|
|
113
|
+
try:
|
|
114
|
+
result = await self._invoke_method(
|
|
115
|
+
"list_videos",
|
|
116
|
+
{"album": album},
|
|
117
|
+
timeout=15.0,
|
|
118
|
+
)
|
|
119
|
+
payload = json.loads(result) if result else {}
|
|
120
|
+
videos = payload.get("videos") or []
|
|
121
|
+
return videos if isinstance(videos, list) else []
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"[MediaScanner] list_videos error: {e}")
|
|
124
|
+
return []
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flet-media-scanner
|
|
3
|
+
Version: 0.86.0
|
|
4
|
+
Summary: Flet extension for Android MediaStore — scan, save, list, and delete videos in the Gallery
|
|
5
|
+
Author-email: Fazi-Gondal <nextinpk@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fazi-gondal/Vidsaver
|
|
8
|
+
Project-URL: Repository, https://github.com/fazi-gondal/Vidsaver
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/fazi-gondal/Vidsaver/issues
|
|
10
|
+
Keywords: flet,android,media,mediascanner,gallery,flutter
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: flet>=0.86.0
|
|
21
|
+
|
|
22
|
+
# flet-media-scanner
|
|
23
|
+
|
|
24
|
+
A [Flet](https://flet.dev) extension for Android **MediaStore** integration — save, list, and delete videos in the device Gallery without requiring broad storage permissions.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- **Platform**: Android only (no-op on other platforms)
|
|
29
|
+
- **Android**: API 23+ (minSdk 23)
|
|
30
|
+
- **Flet**: >= 0.86.0
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install flet-media-scanner
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
Register the extension in your `pyproject.toml`:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[tool.flet.extensions]
|
|
44
|
+
flet_media_scanner = "flet_media_scanner.Extension"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import flet as ft
|
|
51
|
+
from flet_media_scanner import MediaScanner, SaveResult
|
|
52
|
+
|
|
53
|
+
async def main(page: ft.Page):
|
|
54
|
+
scanner = MediaScanner()
|
|
55
|
+
page.services.append(scanner)
|
|
56
|
+
page.update()
|
|
57
|
+
|
|
58
|
+
# Save a video to the Gallery (Movies/MyApp folder)
|
|
59
|
+
result: SaveResult = await scanner.save_video(
|
|
60
|
+
"/data/user/0/com.example.app/cache/video.mp4",
|
|
61
|
+
file_name="my_video.mp4",
|
|
62
|
+
album="MyApp",
|
|
63
|
+
)
|
|
64
|
+
if result.success:
|
|
65
|
+
print(f"Saved: {result.content_uri}")
|
|
66
|
+
else:
|
|
67
|
+
print(f"Error: {result.error}")
|
|
68
|
+
|
|
69
|
+
# List videos in the album
|
|
70
|
+
videos = await scanner.list_videos(album="MyApp")
|
|
71
|
+
for v in videos:
|
|
72
|
+
print(v["display_name"], v["content_uri"])
|
|
73
|
+
|
|
74
|
+
# Delete a video by content URI
|
|
75
|
+
deleted = await scanner.delete_video("content://media/external/video/media/123")
|
|
76
|
+
print("Deleted:", deleted)
|
|
77
|
+
|
|
78
|
+
ft.run(main)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API
|
|
82
|
+
|
|
83
|
+
### `MediaScanner` (Service)
|
|
84
|
+
|
|
85
|
+
Add to `page.services` before calling any methods.
|
|
86
|
+
|
|
87
|
+
#### `await save_video(file_path, file_name=None, album="MyApp") → SaveResult`
|
|
88
|
+
|
|
89
|
+
Copies a private app file into Android MediaStore (`Movies/<album>`). Works on Android 10+ without broad storage permissions.
|
|
90
|
+
|
|
91
|
+
| Parameter | Type | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `file_path` | `str` | Absolute path to the source video file |
|
|
94
|
+
| `file_name` | `str \| None` | Display name in Gallery (defaults to basename) |
|
|
95
|
+
| `album` | `str` | Subfolder inside `Movies/` |
|
|
96
|
+
|
|
97
|
+
#### `await list_videos(album="MyApp") → list[dict]`
|
|
98
|
+
|
|
99
|
+
Returns all videos previously saved to the album. Each dict contains: `display_name`, `content_uri`, `mime_type`, `relative_path`, `size`, `date_added`.
|
|
100
|
+
|
|
101
|
+
#### `await delete_video(content_uri) → bool`
|
|
102
|
+
|
|
103
|
+
Deletes a MediaStore item by its `content://` URI.
|
|
104
|
+
|
|
105
|
+
#### `await scan_media(file_path) → bool`
|
|
106
|
+
|
|
107
|
+
*(Legacy)* Triggers a media scan for a file that already exists in public storage. Use `save_video()` for new downloads instead.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `SaveResult` (dataclass)
|
|
112
|
+
|
|
113
|
+
| Field | Type | Description |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `success` | `bool` | Whether the operation succeeded |
|
|
116
|
+
| `content_uri` | `str` | MediaStore `content://` URI |
|
|
117
|
+
| `display_name` | `str` | File name as shown in Gallery |
|
|
118
|
+
| `mime_type` | `str` | e.g. `video/mp4` |
|
|
119
|
+
| `relative_path` | `str` | e.g. `Movies/MyApp/` |
|
|
120
|
+
| `size` | `int` | File size in bytes |
|
|
121
|
+
| `error` | `str` | Error message if `success=False` |
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/flet_media_scanner/__init__.py
|
|
4
|
+
src/flet_media_scanner/media_scanner.py
|
|
5
|
+
src/flet_media_scanner.egg-info/PKG-INFO
|
|
6
|
+
src/flet_media_scanner.egg-info/SOURCES.txt
|
|
7
|
+
src/flet_media_scanner.egg-info/dependency_links.txt
|
|
8
|
+
src/flet_media_scanner.egg-info/requires.txt
|
|
9
|
+
src/flet_media_scanner.egg-info/top_level.txt
|
|
10
|
+
src/flutter/flet_media_scanner/pubspec.yaml
|
|
11
|
+
src/flutter/flet_media_scanner/android/build.gradle
|
|
12
|
+
src/flutter/flet_media_scanner/android/src/main/AndroidManifest.xml
|
|
13
|
+
src/flutter/flet_media_scanner/android/src/main/kotlin/com/example/flet_media_scanner/FletMediaScannerPlugin.kt
|
|
14
|
+
src/flutter/flet_media_scanner/lib/flet_media_scanner.dart
|
|
15
|
+
src/flutter/flet_media_scanner/lib/src/extension.dart
|
|
16
|
+
src/flutter/flet_media_scanner/lib/src/media_scanner_service.dart
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flet>=0.86.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
group 'com.example.flet_media_scanner'
|
|
2
|
+
version '1.0'
|
|
3
|
+
|
|
4
|
+
buildscript {
|
|
5
|
+
ext.kotlin_version = '1.8.22'
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
dependencies {
|
|
11
|
+
classpath 'com.android.tools.build:gradle:7.3.0'
|
|
12
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
allprojects {
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
apply plugin: 'com.android.library'
|
|
24
|
+
apply plugin: 'kotlin-android'
|
|
25
|
+
|
|
26
|
+
android {
|
|
27
|
+
namespace "com.example.flet_media_scanner"
|
|
28
|
+
compileSdkVersion 33
|
|
29
|
+
|
|
30
|
+
compileOptions {
|
|
31
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
32
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
kotlinOptions {
|
|
36
|
+
jvmTarget = '1.8'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
defaultConfig {
|
|
40
|
+
minSdkVersion 21
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dependencies {
|
|
45
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
46
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
package com.example.flet_media_scanner
|
|
2
|
+
|
|
3
|
+
import android.content.ContentUris
|
|
4
|
+
import android.content.ContentValues
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Environment
|
|
9
|
+
import android.provider.MediaStore
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
|
12
|
+
import io.flutter.plugin.common.MethodCall
|
|
13
|
+
import io.flutter.plugin.common.MethodChannel
|
|
14
|
+
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
|
15
|
+
import io.flutter.plugin.common.MethodChannel.Result
|
|
16
|
+
import java.io.File
|
|
17
|
+
import java.io.FileInputStream
|
|
18
|
+
import java.net.URLConnection
|
|
19
|
+
|
|
20
|
+
class FletMediaScannerPlugin : FlutterPlugin, MethodCallHandler {
|
|
21
|
+
private lateinit var channel: MethodChannel
|
|
22
|
+
private lateinit var context: Context
|
|
23
|
+
|
|
24
|
+
companion object {
|
|
25
|
+
private const val TAG = "FletMediaScanner"
|
|
26
|
+
private const val CHANNEL = "flet_media_scanner/scan"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
30
|
+
context = binding.applicationContext
|
|
31
|
+
channel = MethodChannel(binding.binaryMessenger, CHANNEL)
|
|
32
|
+
channel.setMethodCallHandler(this)
|
|
33
|
+
Log.d(TAG, "onAttachedToEngine: channel registered")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
|
37
|
+
channel.setMethodCallHandler(null)
|
|
38
|
+
Log.d(TAG, "onDetachedFromEngine")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun onMethodCall(call: MethodCall, result: Result) {
|
|
42
|
+
when (call.method) {
|
|
43
|
+
"saveVideo" -> saveVideo(call, result)
|
|
44
|
+
"deleteVideo" -> deleteVideo(call, result)
|
|
45
|
+
"listVideos" -> listVideos(call, result)
|
|
46
|
+
else -> result.notImplemented()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private fun saveVideo(call: MethodCall, result: Result) {
|
|
51
|
+
val path = call.argument<String>("path")
|
|
52
|
+
val requestedFileName = call.argument<String>("fileName")
|
|
53
|
+
val album = call.argument<String>("album")
|
|
54
|
+
?.trim()
|
|
55
|
+
?.trim('/')
|
|
56
|
+
?.takeIf { it.isNotBlank() }
|
|
57
|
+
?: "Vidsaver"
|
|
58
|
+
|
|
59
|
+
if (path.isNullOrBlank()) {
|
|
60
|
+
result.error("INVALID_ARGUMENT", "path must not be null or empty", null)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
val source = File(path)
|
|
65
|
+
if (!source.isFile) {
|
|
66
|
+
result.error("FILE_NOT_FOUND", "source file does not exist: $path", null)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
val displayName = requestedFileName
|
|
71
|
+
?.trim()
|
|
72
|
+
?.takeIf { it.isNotBlank() }
|
|
73
|
+
?: source.name
|
|
74
|
+
val mimeType = URLConnection.guessContentTypeFromName(displayName) ?: "video/mp4"
|
|
75
|
+
val relativePath = "${Environment.DIRECTORY_MOVIES}/$album"
|
|
76
|
+
val resolver = context.contentResolver
|
|
77
|
+
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
78
|
+
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
79
|
+
} else {
|
|
80
|
+
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var uri: Uri? = null
|
|
84
|
+
try {
|
|
85
|
+
val values = ContentValues().apply {
|
|
86
|
+
put(MediaStore.Video.Media.DISPLAY_NAME, displayName)
|
|
87
|
+
put(MediaStore.Video.Media.MIME_TYPE, mimeType)
|
|
88
|
+
put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
|
|
89
|
+
put(MediaStore.Video.Media.DATE_MODIFIED, source.lastModified() / 1000)
|
|
90
|
+
put(MediaStore.Video.Media.SIZE, source.length())
|
|
91
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
92
|
+
put(MediaStore.Video.Media.RELATIVE_PATH, relativePath)
|
|
93
|
+
put(MediaStore.Video.Media.IS_PENDING, 1)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
uri = resolver.insert(collection, values)
|
|
98
|
+
?: throw IllegalStateException("MediaStore insert returned null")
|
|
99
|
+
|
|
100
|
+
resolver.openOutputStream(uri)?.use { output ->
|
|
101
|
+
FileInputStream(source).use { input ->
|
|
102
|
+
input.copyTo(output)
|
|
103
|
+
}
|
|
104
|
+
} ?: throw IllegalStateException("Unable to open MediaStore output stream")
|
|
105
|
+
|
|
106
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
107
|
+
val publishedValues = ContentValues().apply {
|
|
108
|
+
put(MediaStore.Video.Media.IS_PENDING, 0)
|
|
109
|
+
put(MediaStore.Video.Media.SIZE, source.length())
|
|
110
|
+
}
|
|
111
|
+
resolver.update(uri, publishedValues, null, null)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
result.success(
|
|
115
|
+
mapOf(
|
|
116
|
+
"success" to true,
|
|
117
|
+
"content_uri" to uri.toString(),
|
|
118
|
+
"display_name" to displayName,
|
|
119
|
+
"mime_type" to mimeType,
|
|
120
|
+
"relative_path" to relativePath,
|
|
121
|
+
"source_path" to path,
|
|
122
|
+
"size" to source.length(),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
} catch (e: Exception) {
|
|
126
|
+
Log.e(TAG, "saveVideo: exception: ${e.message}", e)
|
|
127
|
+
uri?.let {
|
|
128
|
+
try {
|
|
129
|
+
resolver.delete(it, null, null)
|
|
130
|
+
} catch (deleteError: Exception) {
|
|
131
|
+
Log.w(TAG, "saveVideo: failed to delete incomplete item: $deleteError")
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
result.error("SAVE_ERROR", e.message, e.toString())
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private fun deleteVideo(call: MethodCall, result: Result) {
|
|
139
|
+
val contentUri = call.argument<String>("contentUri")
|
|
140
|
+
if (contentUri.isNullOrBlank()) {
|
|
141
|
+
result.error("INVALID_ARGUMENT", "contentUri must not be null or empty", null)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
val uri = Uri.parse(contentUri)
|
|
147
|
+
val deletedRows = context.contentResolver.delete(uri, null, null)
|
|
148
|
+
result.success(
|
|
149
|
+
mapOf(
|
|
150
|
+
"success" to (deletedRows > 0),
|
|
151
|
+
"content_uri" to contentUri,
|
|
152
|
+
"deleted_rows" to deletedRows,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
} catch (e: Exception) {
|
|
156
|
+
Log.e(TAG, "deleteVideo: exception: ${e.message}", e)
|
|
157
|
+
result.error("DELETE_ERROR", e.message, e.toString())
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun listVideos(call: MethodCall, result: Result) {
|
|
162
|
+
val album = call.argument<String>("album")
|
|
163
|
+
?.trim()
|
|
164
|
+
?.trim('/')
|
|
165
|
+
?.takeIf { it.isNotBlank() }
|
|
166
|
+
?: "Vidsaver"
|
|
167
|
+
val resolver = context.contentResolver
|
|
168
|
+
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
169
|
+
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
170
|
+
} else {
|
|
171
|
+
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
val projection = mutableListOf(
|
|
175
|
+
MediaStore.Video.Media._ID,
|
|
176
|
+
MediaStore.Video.Media.DISPLAY_NAME,
|
|
177
|
+
MediaStore.Video.Media.MIME_TYPE,
|
|
178
|
+
MediaStore.Video.Media.SIZE,
|
|
179
|
+
MediaStore.Video.Media.DATE_ADDED,
|
|
180
|
+
MediaStore.Video.Media.DATE_MODIFIED,
|
|
181
|
+
)
|
|
182
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
183
|
+
projection.add(MediaStore.Video.Media.RELATIVE_PATH)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
val selection: String?
|
|
187
|
+
val selectionArgs: Array<String>?
|
|
188
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
189
|
+
selection = "${MediaStore.Video.Media.RELATIVE_PATH} LIKE ?"
|
|
190
|
+
selectionArgs = arrayOf("${Environment.DIRECTORY_MOVIES}/$album%")
|
|
191
|
+
} else {
|
|
192
|
+
selection = null
|
|
193
|
+
selectionArgs = null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
val videos = mutableListOf<Map<String, Any?>>()
|
|
197
|
+
try {
|
|
198
|
+
resolver.query(
|
|
199
|
+
collection,
|
|
200
|
+
projection.toTypedArray(),
|
|
201
|
+
selection,
|
|
202
|
+
selectionArgs,
|
|
203
|
+
"${MediaStore.Video.Media.DATE_ADDED} DESC"
|
|
204
|
+
)?.use { cursor ->
|
|
205
|
+
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
|
|
206
|
+
val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
|
|
207
|
+
val mimeIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE)
|
|
208
|
+
val sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
|
|
209
|
+
val addedIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)
|
|
210
|
+
val modifiedIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED)
|
|
211
|
+
val relativePathIndex = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
212
|
+
cursor.getColumnIndex(MediaStore.Video.Media.RELATIVE_PATH)
|
|
213
|
+
} else {
|
|
214
|
+
-1
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
while (cursor.moveToNext()) {
|
|
218
|
+
val id = cursor.getLong(idIndex)
|
|
219
|
+
val uri = ContentUris.withAppendedId(collection, id)
|
|
220
|
+
val displayName = cursor.getString(nameIndex) ?: continue
|
|
221
|
+
val relativePath = if (relativePathIndex >= 0) {
|
|
222
|
+
cursor.getString(relativePathIndex) ?: ""
|
|
223
|
+
} else {
|
|
224
|
+
"${Environment.DIRECTORY_MOVIES}/$album"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
videos.add(
|
|
228
|
+
mapOf(
|
|
229
|
+
"content_uri" to uri.toString(),
|
|
230
|
+
"display_name" to displayName,
|
|
231
|
+
"mime_type" to (cursor.getString(mimeIndex) ?: "video/mp4"),
|
|
232
|
+
"relative_path" to relativePath.trimEnd('/'),
|
|
233
|
+
"size" to cursor.getLong(sizeIndex),
|
|
234
|
+
"date_added" to cursor.getLong(addedIndex),
|
|
235
|
+
"date_modified" to cursor.getLong(modifiedIndex),
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
result.success(
|
|
242
|
+
mapOf(
|
|
243
|
+
"success" to true,
|
|
244
|
+
"videos" to videos,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
} catch (e: Exception) {
|
|
248
|
+
Log.e(TAG, "listVideos: exception: ${e.message}", e)
|
|
249
|
+
result.error("LIST_ERROR", e.message, e.toString())
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import 'package:flet/flet.dart';
|
|
2
|
+
import 'media_scanner_service.dart';
|
|
3
|
+
|
|
4
|
+
class Extension extends FletExtension {
|
|
5
|
+
@override
|
|
6
|
+
void ensureInitialized() {}
|
|
7
|
+
|
|
8
|
+
@override
|
|
9
|
+
FletService? createService(Control control) {
|
|
10
|
+
switch (control.type) {
|
|
11
|
+
case "MediaScanner":
|
|
12
|
+
return MediaScannerService(control: control);
|
|
13
|
+
default:
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
|
|
3
|
+
import 'package:flet/flet.dart';
|
|
4
|
+
import 'package:flutter/foundation.dart';
|
|
5
|
+
import 'package:flutter/services.dart';
|
|
6
|
+
import 'package:media_scanner/media_scanner.dart';
|
|
7
|
+
|
|
8
|
+
class MediaScannerService extends FletService {
|
|
9
|
+
static const MethodChannel _channel =
|
|
10
|
+
MethodChannel('flet_media_scanner/scan');
|
|
11
|
+
|
|
12
|
+
MediaScannerService({required super.control});
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
void init() {
|
|
16
|
+
super.init();
|
|
17
|
+
control.addInvokeMethodListener(_onInvokeMethod);
|
|
18
|
+
debugPrint("MediaScannerService: initialized");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Future<dynamic> _onInvokeMethod(String methodName, dynamic args) async {
|
|
22
|
+
Map<String, dynamic> arguments = {};
|
|
23
|
+
if (args != null && args is Map) {
|
|
24
|
+
arguments = Map<String, dynamic>.from(args);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
debugPrint(
|
|
28
|
+
"MediaScannerService._onInvokeMethod: $methodName args=$arguments");
|
|
29
|
+
|
|
30
|
+
return switch (methodName) {
|
|
31
|
+
"save_video" => await _saveVideo(arguments),
|
|
32
|
+
"delete_video" => await _deleteVideo(arguments),
|
|
33
|
+
"list_videos" => await _listVideos(arguments),
|
|
34
|
+
"scan_media" => await _scanMedia(arguments),
|
|
35
|
+
_ => throw Exception("MediaScannerService: unknown method '$methodName'"),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Future<String> _saveVideo(Map<String, dynamic> args) async {
|
|
40
|
+
final String? path = args["path"] as String?;
|
|
41
|
+
final String? fileName = args["file_name"] as String?;
|
|
42
|
+
final String album = args["album"] as String? ?? "Vidsaver";
|
|
43
|
+
if (path == null || path.isEmpty) {
|
|
44
|
+
return _encodeResult({
|
|
45
|
+
"success": false,
|
|
46
|
+
"error": "path is required",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
final result = await _channel.invokeMethod<Map<dynamic, dynamic>>(
|
|
52
|
+
'saveVideo',
|
|
53
|
+
{
|
|
54
|
+
'path': path,
|
|
55
|
+
'fileName': fileName,
|
|
56
|
+
'album': album,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
final payload = Map<String, dynamic>.from(result ?? {});
|
|
60
|
+
control.triggerEvent("saved", payload);
|
|
61
|
+
return _encodeResult(payload);
|
|
62
|
+
} on PlatformException catch (e) {
|
|
63
|
+
final payload = {
|
|
64
|
+
"success": false,
|
|
65
|
+
"path": path,
|
|
66
|
+
"error": "${e.code}: ${e.message}",
|
|
67
|
+
};
|
|
68
|
+
control.triggerEvent("saved", payload);
|
|
69
|
+
return _encodeResult(payload);
|
|
70
|
+
} catch (error, stack) {
|
|
71
|
+
debugPrint("MediaScannerService._saveVideo: ERROR '$path': $error\n$stack");
|
|
72
|
+
final payload = {
|
|
73
|
+
"success": false,
|
|
74
|
+
"path": path,
|
|
75
|
+
"error": error.toString(),
|
|
76
|
+
};
|
|
77
|
+
control.triggerEvent("saved", payload);
|
|
78
|
+
return _encodeResult(payload);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Future<String> _scanMedia(Map<String, dynamic> args) async {
|
|
83
|
+
final String? path = args["path"] as String?;
|
|
84
|
+
if (path == null || path.isEmpty) {
|
|
85
|
+
debugPrint("MediaScannerService._scanMedia: path is null or empty");
|
|
86
|
+
control.triggerEvent(
|
|
87
|
+
"scanned", {"path": path ?? "", "success": "false", "error": "path is required"});
|
|
88
|
+
return "false";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
debugPrint("MediaScannerService._scanMedia: scanning '$path'");
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await MediaScanner.loadMedia(path: path);
|
|
95
|
+
debugPrint(
|
|
96
|
+
"MediaScannerService._scanMedia: media_scanner completed path='$path'");
|
|
97
|
+
control.triggerEvent("scanned", {
|
|
98
|
+
"path": path,
|
|
99
|
+
"success": "true",
|
|
100
|
+
"result": "scanned",
|
|
101
|
+
});
|
|
102
|
+
return "true";
|
|
103
|
+
} catch (error, stack) {
|
|
104
|
+
debugPrint(
|
|
105
|
+
"MediaScannerService._scanMedia: ERROR '$path': $error\n$stack");
|
|
106
|
+
control.triggerEvent("scanned", {
|
|
107
|
+
"path": path,
|
|
108
|
+
"success": "false",
|
|
109
|
+
"error": error.toString(),
|
|
110
|
+
});
|
|
111
|
+
return "false";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Future<String> _deleteVideo(Map<String, dynamic> args) async {
|
|
116
|
+
final String? contentUri = args["content_uri"] as String?;
|
|
117
|
+
if (contentUri == null || contentUri.isEmpty) {
|
|
118
|
+
return _encodeResult({
|
|
119
|
+
"success": false,
|
|
120
|
+
"error": "content_uri is required",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
final result = await _channel.invokeMethod<Map<dynamic, dynamic>>(
|
|
126
|
+
'deleteVideo',
|
|
127
|
+
{'contentUri': contentUri},
|
|
128
|
+
);
|
|
129
|
+
final payload = Map<String, dynamic>.from(result ?? {});
|
|
130
|
+
control.triggerEvent("deleted", payload);
|
|
131
|
+
return _encodeResult(payload);
|
|
132
|
+
} on PlatformException catch (e) {
|
|
133
|
+
final payload = {
|
|
134
|
+
"success": false,
|
|
135
|
+
"content_uri": contentUri,
|
|
136
|
+
"error": "${e.code}: ${e.message}",
|
|
137
|
+
};
|
|
138
|
+
control.triggerEvent("deleted", payload);
|
|
139
|
+
return _encodeResult(payload);
|
|
140
|
+
} catch (error, stack) {
|
|
141
|
+
debugPrint("MediaScannerService._deleteVideo: ERROR '$contentUri': $error\n$stack");
|
|
142
|
+
final payload = {
|
|
143
|
+
"success": false,
|
|
144
|
+
"content_uri": contentUri,
|
|
145
|
+
"error": error.toString(),
|
|
146
|
+
};
|
|
147
|
+
control.triggerEvent("deleted", payload);
|
|
148
|
+
return _encodeResult(payload);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Future<String> _listVideos(Map<String, dynamic> args) async {
|
|
153
|
+
final String album = args["album"] as String? ?? "Vidsaver";
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
final result = await _channel.invokeMethod<Map<dynamic, dynamic>>(
|
|
157
|
+
'listVideos',
|
|
158
|
+
{'album': album},
|
|
159
|
+
);
|
|
160
|
+
return _encodeResult(Map<String, dynamic>.from(result ?? {}));
|
|
161
|
+
} on PlatformException catch (e) {
|
|
162
|
+
return _encodeResult({
|
|
163
|
+
"success": false,
|
|
164
|
+
"videos": [],
|
|
165
|
+
"error": "${e.code}: ${e.message}",
|
|
166
|
+
});
|
|
167
|
+
} catch (error, stack) {
|
|
168
|
+
debugPrint("MediaScannerService._listVideos: ERROR '$album': $error\n$stack");
|
|
169
|
+
return _encodeResult({
|
|
170
|
+
"success": false,
|
|
171
|
+
"videos": [],
|
|
172
|
+
"error": error.toString(),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
String _encodeResult(Map<String, dynamic> payload) {
|
|
178
|
+
return jsonEncode(payload);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@override
|
|
182
|
+
void dispose() {
|
|
183
|
+
debugPrint("MediaScannerService: disposed");
|
|
184
|
+
super.dispose();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: flet_media_scanner
|
|
2
|
+
description: Flet extension for Android MediaStore saving with media scanner fallback.
|
|
3
|
+
version: 0.85.3
|
|
4
|
+
publish_to: none
|
|
5
|
+
|
|
6
|
+
environment:
|
|
7
|
+
sdk: ">=3.0.0 <4.0.0"
|
|
8
|
+
flutter: ">=3.0.0"
|
|
9
|
+
|
|
10
|
+
dependencies:
|
|
11
|
+
flutter:
|
|
12
|
+
sdk: flutter
|
|
13
|
+
flet: ^0.85.0
|
|
14
|
+
media_scanner: ^2.2.1
|
|
15
|
+
|
|
16
|
+
flutter:
|
|
17
|
+
plugin:
|
|
18
|
+
platforms:
|
|
19
|
+
android:
|
|
20
|
+
package: com.example.flet_media_scanner
|
|
21
|
+
pluginClass: FletMediaScannerPlugin
|