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.
@@ -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
+ ![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,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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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