syncfotos 0.1.6__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.
Files changed (31) hide show
  1. syncfotos-0.1.6/.github/workflows/release.yml +58 -0
  2. syncfotos-0.1.6/.gitignore +20 -0
  3. syncfotos-0.1.6/PKG-INFO +212 -0
  4. syncfotos-0.1.6/README.md +198 -0
  5. syncfotos-0.1.6/duplicats.py +11 -0
  6. syncfotos-0.1.6/fitxers_buits.py +6 -0
  7. syncfotos-0.1.6/pyproject.toml +40 -0
  8. syncfotos-0.1.6/setup.cfg +4 -0
  9. syncfotos-0.1.6/src/syncfotos/__init__.py +1 -0
  10. syncfotos-0.1.6/src/syncfotos/core/__init__.py +14 -0
  11. syncfotos-0.1.6/src/syncfotos/core/duplicats_core.py +135 -0
  12. syncfotos-0.1.6/src/syncfotos/core/review_ops.py +70 -0
  13. syncfotos-0.1.6/src/syncfotos/core/sync_core.py +256 -0
  14. syncfotos-0.1.6/src/syncfotos/core/sync_outputs.py +51 -0
  15. syncfotos-0.1.6/src/syncfotos/sync_cli.py +156 -0
  16. syncfotos-0.1.6/src/syncfotos/sync_gui.py +11 -0
  17. syncfotos-0.1.6/src/syncfotos/tools/__init__.py +14 -0
  18. syncfotos-0.1.6/src/syncfotos/tools/duplicats.py +13 -0
  19. syncfotos-0.1.6/src/syncfotos/tools/fitxers_buits.py +118 -0
  20. syncfotos-0.1.6/src/syncfotos/ui/__init__.py +6 -0
  21. syncfotos-0.1.6/src/syncfotos/ui/duplicats_gui.py +505 -0
  22. syncfotos-0.1.6/src/syncfotos/ui/review_gui.py +500 -0
  23. syncfotos-0.1.6/src/syncfotos/ui/sync_fotos_gui.py +306 -0
  24. syncfotos-0.1.6/src/syncfotos.egg-info/PKG-INFO +212 -0
  25. syncfotos-0.1.6/src/syncfotos.egg-info/SOURCES.txt +29 -0
  26. syncfotos-0.1.6/src/syncfotos.egg-info/dependency_links.txt +1 -0
  27. syncfotos-0.1.6/src/syncfotos.egg-info/entry_points.txt +5 -0
  28. syncfotos-0.1.6/src/syncfotos.egg-info/requires.txt +3 -0
  29. syncfotos-0.1.6/src/syncfotos.egg-info/top_level.txt +1 -0
  30. syncfotos-0.1.6/sync_fotos.py +140 -0
  31. syncfotos-0.1.6/sync_fotos_gui.py +8 -0
@@ -0,0 +1,58 @@
1
+ name: Build and Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v7
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - name: Setup Python
21
+ uses: actions/setup-python@v6
22
+ with:
23
+ python-version: "3.11"
24
+
25
+ - name: Install build backend
26
+ run: python -m pip install --upgrade build
27
+
28
+ - name: Build wheel and sdist
29
+ shell: bash
30
+ run: |
31
+ if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
32
+ export SETUPTOOLS_SCM_PRETEND_VERSION="${GITHUB_REF_NAME#v}"
33
+ echo "Using tag version: ${SETUPTOOLS_SCM_PRETEND_VERSION}"
34
+ fi
35
+ python -m build
36
+
37
+ - name: Upload build artifacts
38
+ uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/*
42
+
43
+ publish-pypi:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ if: startsWith(github.ref, 'refs/tags/v')
47
+ permissions:
48
+ id-token: write
49
+ contents: read
50
+ steps:
51
+ - name: Download build artifacts
52
+ uses: actions/download-artifact@v4
53
+ with:
54
+ name: dist
55
+ path: dist
56
+
57
+ - name: Publish to PyPI (Trusted Publishing)
58
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+
4
+ .venv/
5
+ venv/
6
+
7
+ .vscode/
8
+ .idea/
9
+
10
+ build/
11
+ dist/
12
+
13
+ .DS_Store
14
+ Thumbs.db
15
+
16
+ *.log
17
+
18
+ dest/
19
+ source/
20
+ cache/
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.4
2
+ Name: syncfotos
3
+ Version: 0.1.6
4
+ Summary: Eina per comparar, revisar i classificar fotografies i videos entre directoris.
5
+ Author: SyncFotos
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AureliRoura/SyncFotos
8
+ Project-URL: Repository, https://github.com/AureliRoura/SyncFotos
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pillow>=10.0.0
12
+ Requires-Dist: python-vlc>=3.0.0
13
+ Requires-Dist: opencv-python>=4.8.0
14
+
15
+ # SyncFotos
16
+
17
+ Aplicació en Python per sincronitzar, revisar i classificar fotografies i vídeos entre dos directoris.
18
+
19
+ L'objectiu és detectar els fitxers que existeixen al directori d'origen però no al de destinació i facilitar-ne la revisió abans de moure'ls a la ubicació definitiva.
20
+
21
+ ---
22
+
23
+ ## Característiques
24
+
25
+ * 📷 Visualització d'imatges.
26
+ * 🎥 Reproducció de vídeos integrada amb VLC.
27
+ * ⏯ Controls de reproducció (Play/Pausa i barra de progrés).
28
+ * 🔍 Comparació entre dos directoris.
29
+ * 📂 Detecció de fitxers inexistents al directori de destinació.
30
+ * 📁 Moure fitxers a una carpeta seleccionada.
31
+ * 📁 Moure fitxers a l'última carpeta utilitzada.
32
+ * 📅 Moure fitxers automàticament segons la data deduïda del nom.
33
+ * 🖼 Obrir una fotografia amb un editor extern.
34
+ * 💬 Tooltips amb informació sobre el directori de destinació.
35
+
36
+ ---
37
+
38
+ ## Organització automàtica per data
39
+
40
+ Si el fitxer té un nom com:
41
+
42
+ ```text
43
+ PXL_20251129_080003803.TS.mp4
44
+ ```
45
+
46
+ SyncFotos detecta la data (`2025-11-29`) i proposa moure'l a:
47
+
48
+ ```text
49
+ 2025/
50
+ └── 2025_11_29/
51
+ ```
52
+
53
+ Si el nom del fitxer no conté una data reconeguda, aquesta opció no es mostra.
54
+
55
+ ---
56
+
57
+ ## Requisits
58
+
59
+ * Python 3.11 o superior
60
+ * VLC Media Player instal·lat
61
+
62
+ Llibreries Python:
63
+
64
+ ```bash
65
+ pip install pillow python-vlc opencv-python
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Execució
71
+
72
+ ```bash
73
+ python sync_fotos.py
74
+ ```
75
+
76
+ Per veure totes les opcions disponibles:
77
+
78
+ ```bash
79
+ python sync_fotos.py --help
80
+ ```
81
+
82
+ També es pot executar com a paquet (un cop instal.lat):
83
+
84
+ ```bash
85
+ sync-fotos --help
86
+ sync-fotos-gui
87
+ sync-fotos-duplicats
88
+ sync-fotos-fitxers-buits "D:\\Fotos"
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Empaquetat i distribució
94
+
95
+ Construcció local del paquet:
96
+
97
+ ```bash
98
+ python -m pip install --upgrade build
99
+ python -m build
100
+ ```
101
+
102
+ Es generen dos artefactes a `dist/`:
103
+
104
+ * `syncfotos-<versio>.tar.gz`
105
+ * `syncfotos-<versio>-py3-none-any.whl`
106
+
107
+ Instal.lació local de prova:
108
+
109
+ ```bash
110
+ python -m pip install dist/syncfotos-*.whl
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Publicació amb GitHub Actions
116
+
117
+ El workflow [`Build and Publish`](.github/workflows/release.yml) fa:
118
+
119
+ 1. Build de wheel + sdist.
120
+ 2. Publicació d'artefactes del build.
121
+ 3. Publicació a PyPI quan fas push d'un tag `v*` (per exemple `v1.0.0`).
122
+
123
+ Passos per activar-ho:
124
+
125
+ 1. A [`pyproject.toml`](pyproject.toml), canvia els URLs `OWNER/REPO` pel teu repositori real.
126
+ 2. A PyPI, crea el projecte `syncfotos` (o canvia el nom a `pyproject.toml` si ja existeix).
127
+ 3. Configura Trusted Publishing de PyPI per aquest repositori GitHub.
128
+ 4. Fes commit i push.
129
+ 5. Crea un tag de release:
130
+
131
+ ```bash
132
+ git tag v1.0.0
133
+ git push origin v1.0.0
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Flux de treball
139
+
140
+ 1. Compara el directori origen amb el directori destinació.
141
+ 2. Detecta els fitxers que falten.
142
+ 3. Mostra cada fitxer (foto o vídeo).
143
+ 4. L'usuari pot:
144
+
145
+ * Moure'l a una carpeta.
146
+ * Moure'l a l'última carpeta utilitzada.
147
+ * Moure'l segons la data del nom.
148
+ * Obrir-lo amb un editor.
149
+ * Saltar-lo.
150
+
151
+ 5. En finalitzar es mostra un resum de les accions realitzades.
152
+
153
+ ---
154
+
155
+ ## Llibreries utilitzades
156
+
157
+ * argparse
158
+ * pathlib
159
+ * shutil
160
+ * tkinter
161
+ * Pillow
162
+ * python-vlc
163
+ * OpenCV
164
+ * datetime
165
+ * re
166
+
167
+ ---
168
+
169
+ ## Estructura del projecte
170
+
171
+ ```text
172
+ syncFotos/
173
+ │
174
+ ├── sync_fotos.py
175
+ ├── README.md
176
+ ├── .gitignore
177
+ ├── cache/
178
+ └── ...
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Funcionalitats previstes
184
+
185
+ * [ ] Configuració persistent.
186
+ * [ ] Selecció de l'editor d'imatges des de la interfície.
187
+ * [ ] Dreceres de teclat.
188
+ * [ ] Millorar la reproducció de vídeos.
189
+ * [ ] Suport per a més formats de noms de fitxer.
190
+ * [ ] Generació d'un executable per a Windows.
191
+
192
+ ---
193
+
194
+ ## Captura de pantalla
195
+
196
+ Es recomana afegir una o més captures de la interfície aquí.
197
+
198
+ ```markdown
199
+ ![Captura principal](docs/screenshot.png)
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Llicència
205
+
206
+ Aquest projecte es distribueix sota la llicència MIT.
207
+
208
+ ---
209
+
210
+ ## Autor
211
+
212
+ Desenvolupat com a eina personal per facilitar l'organització de fotografies i vídeos.
@@ -0,0 +1,198 @@
1
+ # SyncFotos
2
+
3
+ Aplicació en Python per sincronitzar, revisar i classificar fotografies i vídeos entre dos directoris.
4
+
5
+ L'objectiu és detectar els fitxers que existeixen al directori d'origen però no al de destinació i facilitar-ne la revisió abans de moure'ls a la ubicació definitiva.
6
+
7
+ ---
8
+
9
+ ## Característiques
10
+
11
+ * 📷 Visualització d'imatges.
12
+ * 🎥 Reproducció de vídeos integrada amb VLC.
13
+ * ⏯ Controls de reproducció (Play/Pausa i barra de progrés).
14
+ * 🔍 Comparació entre dos directoris.
15
+ * 📂 Detecció de fitxers inexistents al directori de destinació.
16
+ * 📁 Moure fitxers a una carpeta seleccionada.
17
+ * 📁 Moure fitxers a l'última carpeta utilitzada.
18
+ * 📅 Moure fitxers automàticament segons la data deduïda del nom.
19
+ * 🖼 Obrir una fotografia amb un editor extern.
20
+ * 💬 Tooltips amb informació sobre el directori de destinació.
21
+
22
+ ---
23
+
24
+ ## Organització automàtica per data
25
+
26
+ Si el fitxer té un nom com:
27
+
28
+ ```text
29
+ PXL_20251129_080003803.TS.mp4
30
+ ```
31
+
32
+ SyncFotos detecta la data (`2025-11-29`) i proposa moure'l a:
33
+
34
+ ```text
35
+ 2025/
36
+ └── 2025_11_29/
37
+ ```
38
+
39
+ Si el nom del fitxer no conté una data reconeguda, aquesta opció no es mostra.
40
+
41
+ ---
42
+
43
+ ## Requisits
44
+
45
+ * Python 3.11 o superior
46
+ * VLC Media Player instal·lat
47
+
48
+ Llibreries Python:
49
+
50
+ ```bash
51
+ pip install pillow python-vlc opencv-python
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Execució
57
+
58
+ ```bash
59
+ python sync_fotos.py
60
+ ```
61
+
62
+ Per veure totes les opcions disponibles:
63
+
64
+ ```bash
65
+ python sync_fotos.py --help
66
+ ```
67
+
68
+ També es pot executar com a paquet (un cop instal.lat):
69
+
70
+ ```bash
71
+ sync-fotos --help
72
+ sync-fotos-gui
73
+ sync-fotos-duplicats
74
+ sync-fotos-fitxers-buits "D:\\Fotos"
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Empaquetat i distribució
80
+
81
+ Construcció local del paquet:
82
+
83
+ ```bash
84
+ python -m pip install --upgrade build
85
+ python -m build
86
+ ```
87
+
88
+ Es generen dos artefactes a `dist/`:
89
+
90
+ * `syncfotos-<versio>.tar.gz`
91
+ * `syncfotos-<versio>-py3-none-any.whl`
92
+
93
+ Instal.lació local de prova:
94
+
95
+ ```bash
96
+ python -m pip install dist/syncfotos-*.whl
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Publicació amb GitHub Actions
102
+
103
+ El workflow [`Build and Publish`](.github/workflows/release.yml) fa:
104
+
105
+ 1. Build de wheel + sdist.
106
+ 2. Publicació d'artefactes del build.
107
+ 3. Publicació a PyPI quan fas push d'un tag `v*` (per exemple `v1.0.0`).
108
+
109
+ Passos per activar-ho:
110
+
111
+ 1. A [`pyproject.toml`](pyproject.toml), canvia els URLs `OWNER/REPO` pel teu repositori real.
112
+ 2. A PyPI, crea el projecte `syncfotos` (o canvia el nom a `pyproject.toml` si ja existeix).
113
+ 3. Configura Trusted Publishing de PyPI per aquest repositori GitHub.
114
+ 4. Fes commit i push.
115
+ 5. Crea un tag de release:
116
+
117
+ ```bash
118
+ git tag v1.0.0
119
+ git push origin v1.0.0
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Flux de treball
125
+
126
+ 1. Compara el directori origen amb el directori destinació.
127
+ 2. Detecta els fitxers que falten.
128
+ 3. Mostra cada fitxer (foto o vídeo).
129
+ 4. L'usuari pot:
130
+
131
+ * Moure'l a una carpeta.
132
+ * Moure'l a l'última carpeta utilitzada.
133
+ * Moure'l segons la data del nom.
134
+ * Obrir-lo amb un editor.
135
+ * Saltar-lo.
136
+
137
+ 5. En finalitzar es mostra un resum de les accions realitzades.
138
+
139
+ ---
140
+
141
+ ## Llibreries utilitzades
142
+
143
+ * argparse
144
+ * pathlib
145
+ * shutil
146
+ * tkinter
147
+ * Pillow
148
+ * python-vlc
149
+ * OpenCV
150
+ * datetime
151
+ * re
152
+
153
+ ---
154
+
155
+ ## Estructura del projecte
156
+
157
+ ```text
158
+ syncFotos/
159
+ │
160
+ ├── sync_fotos.py
161
+ ├── README.md
162
+ ├── .gitignore
163
+ ├── cache/
164
+ └── ...
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Funcionalitats previstes
170
+
171
+ * [ ] Configuració persistent.
172
+ * [ ] Selecció de l'editor d'imatges des de la interfície.
173
+ * [ ] Dreceres de teclat.
174
+ * [ ] Millorar la reproducció de vídeos.
175
+ * [ ] Suport per a més formats de noms de fitxer.
176
+ * [ ] Generació d'un executable per a Windows.
177
+
178
+ ---
179
+
180
+ ## Captura de pantalla
181
+
182
+ Es recomana afegir una o més captures de la interfície aquí.
183
+
184
+ ```markdown
185
+ ![Captura principal](docs/screenshot.png)
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Llicència
191
+
192
+ Aquest projecte es distribueix sota la llicència MIT.
193
+
194
+ ---
195
+
196
+ ## Autor
197
+
198
+ Desenvolupat com a eina personal per facilitar l'organització de fotografies i vídeos.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+ from pathlib import Path
5
+
6
+ from src.syncfotos.tools.duplicats import DuplicatsApp
7
+
8
+
9
+ if __name__ == "__main__":
10
+ cache_arg = sys.argv[1] if len(sys.argv) > 1 else None
11
+ DuplicatsApp(cache_arg)
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ from src.syncfotos.tools.fitxers_buits import main
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "setuptools-scm>=8", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "syncfotos"
7
+ dynamic = ["version"]
8
+ description = "Eina per comparar, revisar i classificar fotografies i videos entre directoris."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "SyncFotos" }
14
+ ]
15
+ dependencies = [
16
+ "pillow>=10.0.0",
17
+ "python-vlc>=3.0.0",
18
+ "opencv-python>=4.8.0"
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/AureliRoura/SyncFotos"
23
+ Repository = "https://github.com/AureliRoura/SyncFotos"
24
+
25
+ [project.scripts]
26
+ sync-fotos = "syncfotos.sync_cli:main"
27
+ sync-fotos-gui = "syncfotos.sync_gui:main"
28
+ sync-fotos-duplicats = "syncfotos.tools.duplicats:main"
29
+ sync-fotos-fitxers-buits = "syncfotos.tools.fitxers_buits:main"
30
+
31
+ [tool.setuptools]
32
+ package-dir = {"" = "src"}
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools_scm]
38
+ tag_regex = "^v(?P<version>\\d+\\.\\d+\\.\\d+.*)$"
39
+ fallback_version = "0.0.0"
40
+ local_scheme = "no-local-version"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Paquet principal de SyncFotos."""
@@ -0,0 +1,14 @@
1
+ """Nucli de lògica de SyncFotos."""
2
+
3
+ from .sync_core import (
4
+ cache_path_for,
5
+ count_files,
6
+ data_a_path,
7
+ default_cache_dir,
8
+ load_cache,
9
+ load_validated_cache,
10
+ scan_directory,
11
+ save_cache,
12
+ )
13
+ from .sync_outputs import build_missing_and_present, write_deletion_script, write_missing_report
14
+
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import platform
5
+ import subprocess
6
+ from collections import defaultdict
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ import ctypes
10
+
11
+
12
+ IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp", ".tiff", ".tif"}
13
+ VIDEO_EXTS = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v", ".3gp", ".flv", ".wmv", ".mts", ".m2ts", ".ts"}
14
+
15
+
16
+ def load_cache(path: Path):
17
+ with open(path, "r", encoding="utf-8") as f:
18
+ return json.load(f)
19
+
20
+
21
+ def find_duplicate_groups(cache_data, root_dir: Path):
22
+ """
23
+ Retorna una llista de tuples (checksum, [rel_path, rel_path, ...])
24
+ per a cada grup de fotos/vídeos duplicats trobats a la cache.
25
+
26
+ Només es consideren fitxers amb extensions d'imatge o vídeo, i
27
+ només els que realment existeixen al disc en aquest moment.
28
+ """
29
+ fitxers = cache_data.get("fitxers", {})
30
+ media_exts = IMAGE_EXTS | VIDEO_EXTS
31
+ by_checksum = defaultdict(list)
32
+ missing_count = 0
33
+
34
+ for rel_path, info in fitxers.items():
35
+ if Path(rel_path).suffix.lower() not in media_exts:
36
+ continue
37
+ checksum = info.get("checksum")
38
+ if not checksum:
39
+ continue
40
+ if not (root_dir / rel_path).exists():
41
+ missing_count += 1
42
+ continue
43
+ by_checksum[checksum].append(rel_path)
44
+
45
+ groups = []
46
+ for checksum, paths in by_checksum.items():
47
+ if len(paths) >= 2:
48
+ groups.append((checksum, sorted(paths)))
49
+ return groups, missing_count
50
+
51
+
52
+ def open_with_default_app(path: Path):
53
+ """Obre un fitxer amb l'aplicació per defecte del sistema."""
54
+ system = platform.system()
55
+ if system == "Windows":
56
+ shell32 = ctypes.windll.shell32
57
+ kernel32 = ctypes.windll.kernel32
58
+
59
+ class SHELLEXECUTEINFO(ctypes.Structure):
60
+ _fields_ = [
61
+ ("cbSize", ctypes.c_ulong),
62
+ ("fMask", ctypes.c_ulong),
63
+ ("hwnd", ctypes.c_void_p),
64
+ ("lpVerb", ctypes.c_wchar_p),
65
+ ("lpFile", ctypes.c_wchar_p),
66
+ ("lpParameters", ctypes.c_wchar_p),
67
+ ("lpDirectory", ctypes.c_wchar_p),
68
+ ("nShow", ctypes.c_int),
69
+ ("hInstApp", ctypes.c_void_p),
70
+ ("lpIDList", ctypes.c_void_p),
71
+ ("lpClass", ctypes.c_wchar_p),
72
+ ("hkeyClass", ctypes.c_void_p),
73
+ ("dwHotKey", ctypes.c_ulong),
74
+ ("hIconOrMonitor", ctypes.c_void_p),
75
+ ("hProcess", ctypes.c_void_p),
76
+ ]
77
+
78
+ info = SHELLEXECUTEINFO()
79
+ info.cbSize = ctypes.sizeof(SHELLEXECUTEINFO)
80
+ info.fMask = 0x00000040 # SEE_MASK_NOCLOSEPROCESS
81
+ info.lpVerb = "open"
82
+ info.lpFile = str(path)
83
+ info.nShow = 1
84
+
85
+ if not shell32.ShellExecuteExW(ctypes.byref(info)):
86
+ raise OSError(f"No s'ha pogut obrir '{path}'")
87
+
88
+ return info.hProcess
89
+ elif system == "Darwin":
90
+ return subprocess.Popen(["open", str(path)])
91
+ else:
92
+ return subprocess.Popen(["xdg-open", str(path)])
93
+
94
+
95
+ def stop_opened_app(handle):
96
+ if handle is None:
97
+ return
98
+
99
+ if hasattr(handle, "poll"):
100
+ try:
101
+ if handle.poll() is None:
102
+ handle.terminate()
103
+ try:
104
+ handle.wait(timeout=1)
105
+ except Exception:
106
+ handle.kill()
107
+ except Exception:
108
+ pass
109
+ return
110
+
111
+ if platform.system() == "Windows":
112
+ kernel32 = ctypes.windll.kernel32
113
+ try:
114
+ kernel32.TerminateProcess(ctypes.c_void_p(handle), 0)
115
+ except Exception:
116
+ pass
117
+ try:
118
+ kernel32.CloseHandle(ctypes.c_void_p(handle))
119
+ except Exception:
120
+ pass
121
+
122
+
123
+ def format_size(size_bytes):
124
+ if size_bytes >= 1024 * 1024:
125
+ return f"{size_bytes / 1024 / 1024:.1f} MB"
126
+ if size_bytes >= 1024:
127
+ return f"{size_bytes / 1024:.1f} KB"
128
+ return f"{size_bytes} B"
129
+
130
+
131
+ def format_mtime(mtime):
132
+ try:
133
+ return datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
134
+ except Exception:
135
+ return "Desconegut"