syncfotos 1.0.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.
- syncfotos-1.0.0/PKG-INFO +212 -0
- syncfotos-1.0.0/README.md +198 -0
- syncfotos-1.0.0/pyproject.toml +35 -0
- syncfotos-1.0.0/setup.cfg +4 -0
- syncfotos-1.0.0/src/syncfotos/__init__.py +1 -0
- syncfotos-1.0.0/src/syncfotos/core/__init__.py +13 -0
- syncfotos-1.0.0/src/syncfotos/core/duplicats_core.py +75 -0
- syncfotos-1.0.0/src/syncfotos/core/review_ops.py +70 -0
- syncfotos-1.0.0/src/syncfotos/core/sync_core.py +227 -0
- syncfotos-1.0.0/src/syncfotos/core/sync_outputs.py +51 -0
- syncfotos-1.0.0/src/syncfotos/sync_cli.py +153 -0
- syncfotos-1.0.0/src/syncfotos/sync_gui.py +11 -0
- syncfotos-1.0.0/src/syncfotos/tools/__init__.py +14 -0
- syncfotos-1.0.0/src/syncfotos/tools/duplicats.py +13 -0
- syncfotos-1.0.0/src/syncfotos/tools/fitxers_buits.py +118 -0
- syncfotos-1.0.0/src/syncfotos/ui/__init__.py +6 -0
- syncfotos-1.0.0/src/syncfotos/ui/duplicats_gui.py +495 -0
- syncfotos-1.0.0/src/syncfotos/ui/review_gui.py +494 -0
- syncfotos-1.0.0/src/syncfotos/ui/sync_fotos_gui.py +248 -0
- syncfotos-1.0.0/src/syncfotos.egg-info/PKG-INFO +212 -0
- syncfotos-1.0.0/src/syncfotos.egg-info/SOURCES.txt +23 -0
- syncfotos-1.0.0/src/syncfotos.egg-info/dependency_links.txt +1 -0
- syncfotos-1.0.0/src/syncfotos.egg-info/entry_points.txt +5 -0
- syncfotos-1.0.0/src/syncfotos.egg-info/requires.txt +3 -0
- syncfotos-1.0.0/src/syncfotos.egg-info/top_level.txt +1 -0
syncfotos-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syncfotos
|
|
3
|
+
Version: 1.0.0
|
|
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
|
+

|
|
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
|
+

|
|
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,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "syncfotos"
|
|
7
|
+
version = "1.0.0"
|
|
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"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Paquet principal de SyncFotos."""
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
load_cache,
|
|
8
|
+
load_validated_cache,
|
|
9
|
+
scan_directory,
|
|
10
|
+
save_cache,
|
|
11
|
+
)
|
|
12
|
+
from .sync_outputs import build_missing_and_present, write_deletion_script, write_missing_report
|
|
13
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
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
|
+
os.startfile(str(path)) # type: ignore[attr-defined]
|
|
57
|
+
elif system == "Darwin":
|
|
58
|
+
subprocess.Popen(["open", str(path)])
|
|
59
|
+
else:
|
|
60
|
+
subprocess.Popen(["xdg-open", str(path)])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def format_size(size_bytes):
|
|
64
|
+
if size_bytes >= 1024 * 1024:
|
|
65
|
+
return f"{size_bytes / 1024 / 1024:.1f} MB"
|
|
66
|
+
if size_bytes >= 1024:
|
|
67
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
68
|
+
return f"{size_bytes} B"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_mtime(mtime):
|
|
72
|
+
try:
|
|
73
|
+
return datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
|
74
|
+
except Exception:
|
|
75
|
+
return "Desconegut"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ReviewState:
|
|
10
|
+
idx: int = 0
|
|
11
|
+
last_dir: Path | None = None
|
|
12
|
+
mogudes: int = 0
|
|
13
|
+
saltades: int = 0
|
|
14
|
+
historial: list[tuple[Path, Path]] = field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_review_state(target_root: Path) -> ReviewState:
|
|
18
|
+
return ReviewState(last_dir=target_root)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def unique_destination(dest_dir: Path, source_name: str) -> Path:
|
|
22
|
+
dest_file = dest_dir / source_name
|
|
23
|
+
if not dest_file.exists():
|
|
24
|
+
return dest_file
|
|
25
|
+
|
|
26
|
+
stem = Path(source_name).stem
|
|
27
|
+
suffix = Path(source_name).suffix
|
|
28
|
+
index = 1
|
|
29
|
+
while dest_file.exists():
|
|
30
|
+
dest_file = dest_dir / f"{stem}_{index}{suffix}"
|
|
31
|
+
index += 1
|
|
32
|
+
return dest_file
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def move_missing_file(source_root: Path, rel_path: Path, dest_dir: Path, state: ReviewState) -> tuple[Path, Path]:
|
|
36
|
+
full_path = source_root / rel_path
|
|
37
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
dest_file = unique_destination(dest_dir, full_path.name)
|
|
39
|
+
shutil.move(str(full_path), dest_file)
|
|
40
|
+
state.historial.append((full_path, dest_file))
|
|
41
|
+
state.last_dir = dest_dir
|
|
42
|
+
state.mogudes += 1
|
|
43
|
+
state.idx += 1
|
|
44
|
+
return full_path, dest_file
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def restore_recent_moves(selected_moves: list[tuple[Path, Path]], state: ReviewState) -> list[tuple[Path, Path]]:
|
|
48
|
+
restored: list[tuple[Path, Path]] = []
|
|
49
|
+
for orig, dest in selected_moves:
|
|
50
|
+
orig.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
target_name = orig
|
|
52
|
+
if target_name.exists():
|
|
53
|
+
stem = orig.stem
|
|
54
|
+
suffix = orig.suffix
|
|
55
|
+
index = 1
|
|
56
|
+
while target_name.exists():
|
|
57
|
+
target_name = orig.parent / f"{stem}_{index}{suffix}"
|
|
58
|
+
index += 1
|
|
59
|
+
shutil.move(str(dest), target_name)
|
|
60
|
+
restored.append((orig, dest))
|
|
61
|
+
|
|
62
|
+
for orig, dest in restored:
|
|
63
|
+
try:
|
|
64
|
+
state.historial.remove((orig, dest))
|
|
65
|
+
except ValueError:
|
|
66
|
+
pass
|
|
67
|
+
state.mogudes -= 1
|
|
68
|
+
state.idx = max(0, state.idx - 1)
|
|
69
|
+
|
|
70
|
+
return restored
|