sebsync 1.0a4__tar.gz → 1.0a6__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,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sebsync
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: Synchronize Standard Ebooks catalog with local
|
|
3
|
+
Version: 1.0a6
|
|
4
|
+
Summary: Synchronize Standard Ebooks catalog with a local EPUB collection.
|
|
5
5
|
Home-page: https://github.com/pbryan/sebsync/
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: ebook,downloader,standard,book
|
|
@@ -17,7 +17,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Topic :: Other/Nonlisted Topic
|
|
18
18
|
Requires-Dist: click (>=8.1,<9.0)
|
|
19
19
|
Requires-Dist: requests (>=2.32,<3.0)
|
|
20
|
-
Project-URL: Documentation, https://github.com/pbryan/sebsync/tree/main/docs
|
|
21
20
|
Description-Content-Type: text/markdown
|
|
22
21
|
|
|
23
22
|
# sebsync
|
|
@@ -38,7 +37,7 @@ file.
|
|
|
38
37
|
|
|
39
38
|
The script can also detect extraneous local ebooks (local ebooks not found in the Standard
|
|
40
39
|
Ebooks catalog). This can can occur when Standard Ebooks changes the identifier of a previously
|
|
41
|
-
published ebook. It's a rare
|
|
40
|
+
published ebook. It's a rare occurrence, and it's generally safe to delete such files.
|
|
42
41
|
|
|
43
42
|
## Requirements
|
|
44
43
|
|
|
@@ -16,7 +16,7 @@ file.
|
|
|
16
16
|
|
|
17
17
|
The script can also detect extraneous local ebooks (local ebooks not found in the Standard
|
|
18
18
|
Ebooks catalog). This can can occur when Standard Ebooks changes the identifier of a previously
|
|
19
|
-
published ebook. It's a rare
|
|
19
|
+
published ebook. It's a rare occurrence, and it's generally safe to delete such files.
|
|
20
20
|
|
|
21
21
|
## Requirements
|
|
22
22
|
|
|
@@ -8,12 +8,11 @@ target-version = ['py312']
|
|
|
8
8
|
|
|
9
9
|
[tool.poetry]
|
|
10
10
|
name = "sebsync"
|
|
11
|
-
version = "1.
|
|
12
|
-
description = "Synchronize Standard Ebooks catalog with local
|
|
11
|
+
version = "1.0a6"
|
|
12
|
+
description = "Synchronize Standard Ebooks catalog with a local EPUB collection."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
authors = ["sebsync authors"]
|
|
15
15
|
homepage = "https://github.com/pbryan/sebsync/"
|
|
16
|
-
documentation = "https://github.com/pbryan/sebsync/tree/main/docs"
|
|
17
16
|
license = "MIT"
|
|
18
17
|
keywords = ["ebook", "downloader", "standard", "book"]
|
|
19
18
|
classifiers = [
|
|
@@ -23,15 +23,15 @@ class Status:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
_epilog = f"""
|
|
26
|
-
The following statuses are reported
|
|
26
|
+
The following statuses are reported:
|
|
27
27
|
|
|
28
28
|
\b
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
{Status.NEW}: new (downloads the new ebook to downloads directory)
|
|
30
|
+
{Status.UPDATE}: update (overwrites the existing local ebook with new version)
|
|
31
|
+
{Status.EXTRA}: extraneous (local ebook was not found in Standard Ebooks catalog)
|
|
32
32
|
|
|
33
33
|
An extraneous ebook can occur when Standard Ebooks changes the identifier of a previously
|
|
34
|
-
published ebook. It's a rare
|
|
34
|
+
published ebook. It's a rare occurrence, and it's generally safe to delete such files.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
37
|
|
|
@@ -56,16 +56,24 @@ class LocalEbook:
|
|
|
56
56
|
modified: datetime
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
# map type selection to link title in OPDS catalog
|
|
60
|
+
type_selector = {
|
|
61
|
+
"compatible": "Recommended compatible epub",
|
|
62
|
+
"kobo": "Kobo Kepub epub",
|
|
63
|
+
"advanced": "Advanced epub",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def echo_status(path: Path, status: str) -> None:
|
|
60
68
|
if not _quiet:
|
|
61
69
|
click.echo(f"{status} {path}")
|
|
62
70
|
|
|
63
71
|
|
|
64
|
-
def
|
|
72
|
+
def if_exists(path: Path) -> Path | None:
|
|
65
73
|
return path if path.exists() else None
|
|
66
74
|
|
|
67
75
|
|
|
68
|
-
def
|
|
76
|
+
def fromisoformat(text: str) -> datetime:
|
|
69
77
|
"""Convert RFC 3339 string into datetime; compatible with Python 3.10."""
|
|
70
78
|
if not text.endswith("Z"):
|
|
71
79
|
raise ValueError("expecting RFC 3339 formatted string")
|
|
@@ -75,13 +83,6 @@ def _fromisoformat(text: str) -> datetime:
|
|
|
75
83
|
)
|
|
76
84
|
|
|
77
85
|
|
|
78
|
-
type_selector = {
|
|
79
|
-
"compatible": "Recommended compatible epub",
|
|
80
|
-
"kobo": "Kobo Kepub epub",
|
|
81
|
-
"advanced": "Advanced epub",
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
86
|
def get_remote_ebooks(opds_url: str, email: str, type: str) -> dict[str, StandardEbook]:
|
|
86
87
|
"""Return Standard Ebooks metadata for EPUBs from the OPDS catalog."""
|
|
87
88
|
ns = {"atom": "http://www.w3.org/2005/Atom", "dc": "http://purl.org/dc/terms/"}
|
|
@@ -95,7 +96,7 @@ def get_remote_ebooks(opds_url: str, email: str, type: str) -> dict[str, Standar
|
|
|
95
96
|
title=entry.find("atom:title", ns).text,
|
|
96
97
|
author=entry.find("atom:author", ns).find("atom:name", ns).text,
|
|
97
98
|
href=entry.find(f".//atom:link[@title='{type_selector[type]}']", ns).attrib["href"],
|
|
98
|
-
updated=
|
|
99
|
+
updated=fromisoformat(entry.find("atom:updated", ns).text),
|
|
99
100
|
)
|
|
100
101
|
ebooks[ebook.id] = ebook
|
|
101
102
|
return ebooks
|
|
@@ -125,7 +126,7 @@ def get_local_ebooks(dir: Path) -> dict[str, LocalEbook]:
|
|
|
125
126
|
id=id.text,
|
|
126
127
|
title=metadata.find(".//dc:title", ns).text,
|
|
127
128
|
path=path,
|
|
128
|
-
modified=
|
|
129
|
+
modified=fromisoformat(modified.text),
|
|
129
130
|
)
|
|
130
131
|
ebooks[ebook.id] = ebook
|
|
131
132
|
return ebooks
|
|
@@ -157,47 +158,49 @@ def ebook_filename(ebook: StandardEbook) -> str:
|
|
|
157
158
|
@click.command(help=__doc__, epilog=_epilog)
|
|
158
159
|
@click.option(
|
|
159
160
|
"--books",
|
|
160
|
-
help="
|
|
161
|
+
help="Directory where local books are stored.",
|
|
161
162
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, path_type=Path),
|
|
162
|
-
default=
|
|
163
|
+
default=if_exists(Path.home() / "Books"),
|
|
163
164
|
)
|
|
164
165
|
@click.option(
|
|
165
166
|
"--downloads",
|
|
166
|
-
help="
|
|
167
|
+
help="Directory where new ebooks are downloaded.",
|
|
167
168
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, path_type=Path),
|
|
168
|
-
default=
|
|
169
|
+
default=if_exists(Path.home() / "Downloads"),
|
|
169
170
|
)
|
|
170
171
|
@click.option(
|
|
171
172
|
"--dry-run",
|
|
172
|
-
help="
|
|
173
|
+
help="Perform a trial run with no changes made.",
|
|
173
174
|
is_flag=True,
|
|
174
175
|
)
|
|
175
176
|
@click.option(
|
|
176
177
|
"--email",
|
|
177
|
-
help="
|
|
178
|
+
help="Email address to authenticate with Standard Ebooks.",
|
|
178
179
|
required=True,
|
|
179
180
|
)
|
|
181
|
+
@click.help_option()
|
|
180
182
|
@click.option(
|
|
181
183
|
"--opds",
|
|
182
|
-
help="URL of Standard Ebooks OPDS catalog",
|
|
184
|
+
help="URL of Standard Ebooks OPDS catalog.",
|
|
183
185
|
default="https://standardebooks.org/feeds/opds/all",
|
|
184
186
|
)
|
|
185
187
|
@click.option(
|
|
186
188
|
"--quiet",
|
|
187
|
-
help="
|
|
189
|
+
help="Suppress non-error messages.",
|
|
188
190
|
is_flag=True,
|
|
189
191
|
)
|
|
190
192
|
@click.option(
|
|
191
193
|
"--type",
|
|
192
|
-
type=click.Choice(
|
|
193
|
-
help="EPUB type to download",
|
|
194
|
+
type=click.Choice(type_selector.keys()),
|
|
195
|
+
help="EPUB type to download.",
|
|
194
196
|
default="compatible",
|
|
195
197
|
)
|
|
196
198
|
@click.option(
|
|
197
199
|
"--verbose",
|
|
198
|
-
help="
|
|
200
|
+
help="Increase verbosity.",
|
|
199
201
|
is_flag=True,
|
|
200
202
|
)
|
|
203
|
+
@click.version_option(package_name="sebsync")
|
|
201
204
|
def sebsync(
|
|
202
205
|
books: str,
|
|
203
206
|
downloads: str,
|
|
@@ -213,7 +216,7 @@ def sebsync(
|
|
|
213
216
|
global _quiet
|
|
214
217
|
|
|
215
218
|
_dry_run = dry_run
|
|
216
|
-
_verbose = verbose
|
|
219
|
+
_verbose = verbose and not quiet # quiet wins
|
|
217
220
|
_quiet = quiet
|
|
218
221
|
|
|
219
222
|
remote_ebooks = get_remote_ebooks(opds, email, type)
|
|
@@ -229,16 +232,16 @@ def sebsync(
|
|
|
229
232
|
for remote_ebook in remote_ebooks.values():
|
|
230
233
|
if local_ebook := local_ebooks.get(remote_ebook.id):
|
|
231
234
|
if remote_ebook.updated != local_ebook.modified:
|
|
232
|
-
|
|
235
|
+
echo_status(local_ebook.path, Status.UPDATE)
|
|
233
236
|
download_ebook(remote_ebook.href, local_ebook.path)
|
|
234
237
|
else:
|
|
235
238
|
path = downloads / ebook_filename(remote_ebook)
|
|
236
|
-
|
|
239
|
+
echo_status(path, Status.NEW)
|
|
237
240
|
download_ebook(remote_ebook.href, path)
|
|
238
241
|
|
|
239
242
|
for local_ebook in local_ebooks.values():
|
|
240
243
|
if local_ebook.id not in remote_ebooks:
|
|
241
|
-
|
|
244
|
+
echo_status(local_ebook.path, Status.EXTRA)
|
|
242
245
|
|
|
243
246
|
|
|
244
247
|
def main():
|
|
File without changes
|
|
File without changes
|