sebsync 1.0.2__tar.gz → 1.0.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sebsync
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Synchronize Standard Ebooks catalog with local EPUB collection.
5
5
  Home-page: https://github.com/pbryan/sebsync/
6
6
  License: MIT
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: System :: Archiving :: Mirroring
19
20
  Requires-Dist: click (>=8.1,<9.0)
20
21
  Requires-Dist: requests (>=2.32,<3.0)
@@ -8,7 +8,7 @@ target-version = ['py312']
8
8
 
9
9
  [tool.poetry]
10
10
  name = "sebsync"
11
- version = "1.0.2"
11
+ version = "1.0.3"
12
12
  description = "Synchronize Standard Ebooks catalog with local EPUB collection."
13
13
  readme = "README.md"
14
14
  authors = ["sebsync authors"]
@@ -28,11 +28,11 @@ click = "^8.1"
28
28
  requests = "^2.32"
29
29
 
30
30
  [tool.poetry.dev-dependencies]
31
- black = "^24.8"
31
+ black = "^24.10"
32
32
  isort = "^5.13"
33
- pre-commit = "^3.8"
33
+ pre-commit = "^4.0"
34
34
  pytest = "^8.3"
35
- pytest-cov = "^5.0"
35
+ pytest-cov = "^6.0"
36
36
 
37
37
  [tool.isort]
38
38
  profile = "black"
@@ -44,4 +44,3 @@ no_sections = true
44
44
 
45
45
  [tool.poetry.scripts]
46
46
  sebsync = "sebsync:main"
47
-
@@ -32,8 +32,8 @@ _epilog = f"""
32
32
 
33
33
  \b
34
34
  Reported file statuses:
35
- • {Status.NEW}: new (downloads the new ebook to downloads directory)
36
- • {Status.UPDATE}: update (overwrites the existing local ebook with new version)
35
+ • {Status.NEW}: new (new ebook downloaded to downloads directory)
36
+ • {Status.UPDATE}: update (local ebook updated with newer version)
37
37
  • {Status.EXTRA}: extraneous (local ebook not found in Standard Ebooks catalog)
38
38
  • {Status.UNKNOWN}: unknown (local ebook could not be processed)
39
39
 
@@ -42,7 +42,7 @@ _epilog = f"""
42
42
 
43
43
 
44
44
  @dataclass
45
- class StandardEbook:
45
+ class RemoteEbook:
46
46
  """Metadata for ebook in Standard Ebooks OPDS catalog."""
47
47
 
48
48
  id: str
@@ -89,7 +89,7 @@ def fromisoformat(text: str) -> datetime:
89
89
  )
90
90
 
91
91
 
92
- def get_remote_ebooks(opds_url: str, email: str, type: str) -> dict[str, StandardEbook]:
92
+ def catalog_remote_ebooks(opds_url: str, email: str, type: str) -> dict[str, RemoteEbook]:
93
93
  """Return Standard Ebooks metadata for EPUBs from the OPDS catalog."""
94
94
  ns = {"atom": "http://www.w3.org/2005/Atom", "dc": "http://purl.org/dc/terms/"}
95
95
  ebooks = {}
@@ -97,7 +97,7 @@ def get_remote_ebooks(opds_url: str, email: str, type: str) -> dict[str, Standar
97
97
  response.raw.decode_content = True
98
98
  root = ElementTree.parse(response.raw).getroot()
99
99
  for entry in root.iterfind(".//atom:entry", ns):
100
- ebook = StandardEbook(
100
+ ebook = RemoteEbook(
101
101
  id=entry.find("dc:identifier", ns).text,
102
102
  title=entry.find("atom:title", ns).text,
103
103
  author=entry.find("atom:author", ns).find("atom:name", ns).text,
@@ -110,7 +110,7 @@ def get_remote_ebooks(opds_url: str, email: str, type: str) -> dict[str, Standar
110
110
  return ebooks
111
111
 
112
112
 
113
- def get_local_ebooks(dir: Path) -> dict[str, LocalEbook]:
113
+ def catalog_local_ebooks(dir: Path) -> dict[str, LocalEbook]:
114
114
  """Return metadata of Standard EPUBs in the specified directory and subdirectories."""
115
115
  ebooks = {}
116
116
  for path in dir.glob("**/*.epub"):
@@ -145,8 +145,9 @@ def get_local_ebooks(dir: Path) -> dict[str, LocalEbook]:
145
145
  return ebooks
146
146
 
147
147
 
148
- def download_ebook(url: str, path: Path) -> None:
148
+ def download_ebook(url: str, path: Path, status: str) -> None:
149
149
  """Download the ebook at the specified URL into the specified path."""
150
+ echo_status(path, status)
150
151
  if _dry_run:
151
152
  return
152
153
  download = path.with_suffix(".sebsync")
@@ -176,8 +177,24 @@ def sortable_author(author: str) -> str:
176
177
  return result
177
178
 
178
179
 
179
- def ebook_filename(ebook: StandardEbook) -> str:
180
- """Return an EPUB file name for Standard ebook."""
180
+ def books_are_different(local_ebook: LocalEbook, remote_ebook: RemoteEbook) -> bool:
181
+ """Return if differences between local and remote ebooks are detected."""
182
+
183
+ # if metadata has exact modification times, then local is considered current
184
+ if remote_ebook.updated == local_ebook.modified:
185
+ return False
186
+
187
+ stat = local_ebook.path.stat()
188
+
189
+ # if timestamp of remote is less than that of local file, assume local is current
190
+ if remote_ebook.updated < datetime.fromtimestamp(stat.st_mtime, timezone.utc):
191
+ return False
192
+
193
+ return True
194
+
195
+
196
+ def ebook_filename(ebook: RemoteEbook) -> str:
197
+ """Return an EPUB file name for remote ebook."""
181
198
  replace = {"/": "-", "‘": "'", "’": "'", '"': "'", "“": "'", "”": "'"}
182
199
  match _naming:
183
200
  case "standard":
@@ -204,7 +221,7 @@ def ebook_filename(ebook: StandardEbook) -> str:
204
221
  )
205
222
  @click.option(
206
223
  "--force-update",
207
- help="Force update of all local ebooks.",
224
+ help="Force update of all local ebooks (implies --update).",
208
225
  is_flag=True,
209
226
  )
210
227
  @click.option(
@@ -240,6 +257,11 @@ def ebook_filename(ebook: StandardEbook) -> str:
240
257
  help="EPUB type to download.",
241
258
  default="compatible",
242
259
  )
260
+ @click.option(
261
+ "--update/--no-update",
262
+ help="Download updates into existing local ebook files.",
263
+ default=True,
264
+ )
243
265
  @click.option(
244
266
  "--verbose",
245
267
  help="Increase verbosity.",
@@ -256,8 +278,12 @@ def sebsync(
256
278
  opds: str,
257
279
  quiet: bool,
258
280
  type: str,
281
+ update: bool,
259
282
  verbose: bool,
260
283
  ):
284
+ if force_update:
285
+ update = True
286
+
261
287
  global _dry_run
262
288
  global _verbose
263
289
  global _quiet
@@ -268,23 +294,25 @@ def sebsync(
268
294
  _quiet = quiet
269
295
  _verbose = verbose and not quiet # quiet wins
270
296
 
271
- remote_ebooks = get_remote_ebooks(opds, email, type)
297
+ remote_ebooks = catalog_remote_ebooks(opds, email, type)
272
298
  if _verbose:
273
299
  click.echo(f"Found {len(remote_ebooks)} remote ebooks.")
274
300
 
275
- local_ebooks = get_local_ebooks(downloads) | get_local_ebooks(books)
301
+ local_ebooks = catalog_local_ebooks(books)
276
302
  if _verbose:
277
303
  click.echo(f"Found {len(local_ebooks)} local ebooks.")
278
304
 
279
305
  for remote_ebook in remote_ebooks.values():
280
- if local_ebook := local_ebooks.get(remote_ebook.id):
281
- if remote_ebook.updated != local_ebook.modified or force_update:
282
- echo_status(local_ebook.path, Status.UPDATE)
283
- download_ebook(remote_ebook.href, local_ebook.path)
306
+ local_ebook = local_ebooks.get(remote_ebook.id)
307
+ download_path = downloads / ebook_filename(remote_ebook)
308
+ if local_ebook:
309
+ if force_update or books_are_different(local_ebook, remote_ebook):
310
+ if update:
311
+ download_ebook(remote_ebook.href, local_ebook.path, Status.UPDATE)
312
+ else:
313
+ download_ebook(remote_ebook.href, download_path, Status.NEW)
284
314
  else:
285
- path = downloads / ebook_filename(remote_ebook)
286
- echo_status(path, Status.NEW)
287
- download_ebook(remote_ebook.href, path)
315
+ download_ebook(remote_ebook.href, download_path, Status.NEW)
288
316
 
289
317
  for local_ebook in local_ebooks.values():
290
318
  if local_ebook.id not in remote_ebooks:
File without changes
File without changes
File without changes