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.
|
|
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.
|
|
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.
|
|
31
|
+
black = "^24.10"
|
|
32
32
|
isort = "^5.13"
|
|
33
|
-
pre-commit = "^
|
|
33
|
+
pre-commit = "^4.0"
|
|
34
34
|
pytest = "^8.3"
|
|
35
|
-
pytest-cov = "^
|
|
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 (
|
|
36
|
-
• {Status.UPDATE}: update (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
180
|
-
"""Return
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|