article-backup 0.2.3__tar.gz → 0.3.1__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.
- {article_backup-0.2.3 → article_backup-0.3.1}/PKG-INFO +37 -11
- {article_backup-0.2.3 → article_backup-0.3.1}/README.md +36 -10
- {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/PKG-INFO +37 -11
- {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/SOURCES.txt +1 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/pyproject.toml +1 -1
- {article_backup-0.2.3 → article_backup-0.3.1}/src/boosty.py +35 -5
- {article_backup-0.2.3 → article_backup-0.3.1}/src/database.py +41 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/src/downloader.py +31 -3
- article_backup-0.3.1/src/sponsr.py +655 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/tests/test_asset_dedup.py +6 -1
- article_backup-0.3.1/tests/test_incremental_sync.py +216 -0
- article_backup-0.3.1/tests/test_sponsr_normalize.py +359 -0
- article_backup-0.2.3/src/sponsr.py +0 -349
- article_backup-0.2.3/tests/test_sponsr_normalize.py +0 -153
- {article_backup-0.2.3 → article_backup-0.3.1}/LICENSE +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/dependency_links.txt +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/entry_points.txt +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/requires.txt +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/top_level.txt +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/backup.py +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/setup.cfg +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/src/__init__.py +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/src/config.py +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/src/utils.py +0 -0
- {article_backup-0.2.3 → article_backup-0.3.1}/tests/test_sponsr_tags.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: article-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Локальный бэкап статей с Sponsr.ru и Boosty.to в Markdown с Hugo-интеграцией
|
|
5
5
|
Author-email: Eugene Chaykin <eugene@chayk.in>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -41,16 +41,16 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
## Возможности
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- SQLite
|
|
44
|
+
- **Полный архив статей** одного или нескольких авторов
|
|
45
|
+
- **Инкрементальная синхронизация** — после первой загрузки проверяет только новые посты (⚡ до 98% быстрее повторных запусков)
|
|
46
|
+
- **Конвертация в Markdown** с frontmatter (title, date, tags, source)
|
|
47
|
+
- **Локальное сохранение** изображений, видео, аудио, PDF
|
|
48
|
+
- **Гибкая фильтрация** типов скачиваемых файлов (image, video, audio, document)
|
|
49
|
+
- **Сохранение ссылок** на встроенные видео (Rutube, YouTube, Vimeo, VK, OK.ru)
|
|
50
|
+
- **Нормализация разметки** Sponsr (вложенный em/strong, кавычки, bidi-маркеры)
|
|
51
|
+
- **Исправление внутренних ссылок** между статьями
|
|
52
|
+
- **Интеграция с Hugo** для просмотра в браузере (поддержка тем, улучшенная типографика)
|
|
53
|
+
- **SQLite-индекс** для быстрого поиска
|
|
54
54
|
|
|
55
55
|
## Установка
|
|
56
56
|
|
|
@@ -155,6 +155,32 @@ article-backup "https://boosty.to/author/posts/uuid"
|
|
|
155
155
|
article-backup -c /path/to/config.yaml
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### Инкрементальная синхронизация
|
|
159
|
+
|
|
160
|
+
После первого полного запуска скрипт автоматически переключается в инкрементальный режим:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Первый запуск (полная загрузка):
|
|
164
|
+
[sponsr] Синхронизация author...
|
|
165
|
+
Полная загрузка индекса...
|
|
166
|
+
Получено 2156/2156 постов...
|
|
167
|
+
✓ Архив полностью синхронизирован
|
|
168
|
+
|
|
169
|
+
Повторные запуски (только новые посты):
|
|
170
|
+
[sponsr] Синхронизация author...
|
|
171
|
+
Инкрементальный режим...
|
|
172
|
+
Получено 20/2156 постов... (чанк уже скачан)
|
|
173
|
+
Получено 40/2156 постов... (чанк уже скачан)
|
|
174
|
+
⚡ Остановлено на 40 постах (все новые загружены)
|
|
175
|
+
Найдено постов: 40, новых: 0
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Производительность:**
|
|
179
|
+
- Автор с 2000+ постами: первый запуск ~30 мин, повторные ~30 сек
|
|
180
|
+
- Проверка обновлений для 10 авторов: ~5 мин вместо часов
|
|
181
|
+
|
|
182
|
+
Статус синхронизации хранится в `backup/index.db` (таблица `sync_state`). Старые базы данных автоматически обновляются при первом запуске новой версии.
|
|
183
|
+
|
|
158
184
|
## Разработка
|
|
159
185
|
|
|
160
186
|
### Тесты
|
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
## Возможности
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
- SQLite
|
|
13
|
+
- **Полный архив статей** одного или нескольких авторов
|
|
14
|
+
- **Инкрементальная синхронизация** — после первой загрузки проверяет только новые посты (⚡ до 98% быстрее повторных запусков)
|
|
15
|
+
- **Конвертация в Markdown** с frontmatter (title, date, tags, source)
|
|
16
|
+
- **Локальное сохранение** изображений, видео, аудио, PDF
|
|
17
|
+
- **Гибкая фильтрация** типов скачиваемых файлов (image, video, audio, document)
|
|
18
|
+
- **Сохранение ссылок** на встроенные видео (Rutube, YouTube, Vimeo, VK, OK.ru)
|
|
19
|
+
- **Нормализация разметки** Sponsr (вложенный em/strong, кавычки, bidi-маркеры)
|
|
20
|
+
- **Исправление внутренних ссылок** между статьями
|
|
21
|
+
- **Интеграция с Hugo** для просмотра в браузере (поддержка тем, улучшенная типографика)
|
|
22
|
+
- **SQLite-индекс** для быстрого поиска
|
|
23
23
|
|
|
24
24
|
## Установка
|
|
25
25
|
|
|
@@ -124,6 +124,32 @@ article-backup "https://boosty.to/author/posts/uuid"
|
|
|
124
124
|
article-backup -c /path/to/config.yaml
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Инкрементальная синхронизация
|
|
128
|
+
|
|
129
|
+
После первого полного запуска скрипт автоматически переключается в инкрементальный режим:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Первый запуск (полная загрузка):
|
|
133
|
+
[sponsr] Синхронизация author...
|
|
134
|
+
Полная загрузка индекса...
|
|
135
|
+
Получено 2156/2156 постов...
|
|
136
|
+
✓ Архив полностью синхронизирован
|
|
137
|
+
|
|
138
|
+
Повторные запуски (только новые посты):
|
|
139
|
+
[sponsr] Синхронизация author...
|
|
140
|
+
Инкрементальный режим...
|
|
141
|
+
Получено 20/2156 постов... (чанк уже скачан)
|
|
142
|
+
Получено 40/2156 постов... (чанк уже скачан)
|
|
143
|
+
⚡ Остановлено на 40 постах (все новые загружены)
|
|
144
|
+
Найдено постов: 40, новых: 0
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Производительность:**
|
|
148
|
+
- Автор с 2000+ постами: первый запуск ~30 мин, повторные ~30 сек
|
|
149
|
+
- Проверка обновлений для 10 авторов: ~5 мин вместо часов
|
|
150
|
+
|
|
151
|
+
Статус синхронизации хранится в `backup/index.db` (таблица `sync_state`). Старые базы данных автоматически обновляются при первом запуске новой версии.
|
|
152
|
+
|
|
127
153
|
## Разработка
|
|
128
154
|
|
|
129
155
|
### Тесты
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: article-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Локальный бэкап статей с Sponsr.ru и Boosty.to в Markdown с Hugo-интеграцией
|
|
5
5
|
Author-email: Eugene Chaykin <eugene@chayk.in>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -41,16 +41,16 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
## Возможности
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- SQLite
|
|
44
|
+
- **Полный архив статей** одного или нескольких авторов
|
|
45
|
+
- **Инкрементальная синхронизация** — после первой загрузки проверяет только новые посты (⚡ до 98% быстрее повторных запусков)
|
|
46
|
+
- **Конвертация в Markdown** с frontmatter (title, date, tags, source)
|
|
47
|
+
- **Локальное сохранение** изображений, видео, аудио, PDF
|
|
48
|
+
- **Гибкая фильтрация** типов скачиваемых файлов (image, video, audio, document)
|
|
49
|
+
- **Сохранение ссылок** на встроенные видео (Rutube, YouTube, Vimeo, VK, OK.ru)
|
|
50
|
+
- **Нормализация разметки** Sponsr (вложенный em/strong, кавычки, bidi-маркеры)
|
|
51
|
+
- **Исправление внутренних ссылок** между статьями
|
|
52
|
+
- **Интеграция с Hugo** для просмотра в браузере (поддержка тем, улучшенная типографика)
|
|
53
|
+
- **SQLite-индекс** для быстрого поиска
|
|
54
54
|
|
|
55
55
|
## Установка
|
|
56
56
|
|
|
@@ -155,6 +155,32 @@ article-backup "https://boosty.to/author/posts/uuid"
|
|
|
155
155
|
article-backup -c /path/to/config.yaml
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### Инкрементальная синхронизация
|
|
159
|
+
|
|
160
|
+
После первого полного запуска скрипт автоматически переключается в инкрементальный режим:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Первый запуск (полная загрузка):
|
|
164
|
+
[sponsr] Синхронизация author...
|
|
165
|
+
Полная загрузка индекса...
|
|
166
|
+
Получено 2156/2156 постов...
|
|
167
|
+
✓ Архив полностью синхронизирован
|
|
168
|
+
|
|
169
|
+
Повторные запуски (только новые посты):
|
|
170
|
+
[sponsr] Синхронизация author...
|
|
171
|
+
Инкрементальный режим...
|
|
172
|
+
Получено 20/2156 постов... (чанк уже скачан)
|
|
173
|
+
Получено 40/2156 постов... (чанк уже скачан)
|
|
174
|
+
⚡ Остановлено на 40 постах (все новые загружены)
|
|
175
|
+
Найдено постов: 40, новых: 0
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Производительность:**
|
|
179
|
+
- Автор с 2000+ постами: первый запуск ~30 мин, повторные ~30 сек
|
|
180
|
+
- Проверка обновлений для 10 авторов: ~5 мин вместо часов
|
|
181
|
+
|
|
182
|
+
Статус синхронизации хранится в `backup/index.db` (таблица `sync_state`). Старые базы данных автоматически обновляются при первом запуске новой версии.
|
|
183
|
+
|
|
158
184
|
## Разработка
|
|
159
185
|
|
|
160
186
|
### Тесты
|
|
@@ -28,10 +28,23 @@ class BoostyDownloader(BaseDownloader):
|
|
|
28
28
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
def fetch_posts_list(
|
|
32
|
-
|
|
31
|
+
def fetch_posts_list(
|
|
32
|
+
self,
|
|
33
|
+
existing_ids: set[str] | None = None,
|
|
34
|
+
incremental: bool = False,
|
|
35
|
+
safety_chunks: int = 1
|
|
36
|
+
) -> list[dict]:
|
|
37
|
+
"""
|
|
38
|
+
Получает список постов через API.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
existing_ids: Множество уже загруженных post_id (для инкрементального режима)
|
|
42
|
+
incremental: Включить инкрементальный режим
|
|
43
|
+
safety_chunks: Количество "защитных" чанков перед остановкой
|
|
44
|
+
"""
|
|
33
45
|
all_posts = []
|
|
34
46
|
offset = None
|
|
47
|
+
clean_chunks_count = 0 # Счётчик "чистых" чанков
|
|
35
48
|
|
|
36
49
|
while True:
|
|
37
50
|
url = f"{self.API_BASE}/blog/{self.source.author}/post/?limit=20"
|
|
@@ -48,7 +61,24 @@ class BoostyDownloader(BaseDownloader):
|
|
|
48
61
|
break
|
|
49
62
|
|
|
50
63
|
all_posts.extend(posts_chunk)
|
|
51
|
-
|
|
64
|
+
|
|
65
|
+
# Инкрементальный режим: проверяем, все ли посты уже существуют
|
|
66
|
+
if incremental and existing_ids is not None:
|
|
67
|
+
chunk_ids = {p.get("id") for p in posts_chunk}
|
|
68
|
+
all_existing = chunk_ids.issubset(existing_ids)
|
|
69
|
+
|
|
70
|
+
if all_existing:
|
|
71
|
+
clean_chunks_count += 1
|
|
72
|
+
print(f" Получено {len(all_posts)} постов... (чанк уже скачан)")
|
|
73
|
+
# Останавливаемся после safety_chunks + 1 (первый чистый + N защитных)
|
|
74
|
+
if clean_chunks_count > safety_chunks:
|
|
75
|
+
print(f" ⚡ Остановлено на {len(all_posts)} постах (все новые загружены)")
|
|
76
|
+
break
|
|
77
|
+
else:
|
|
78
|
+
clean_chunks_count = 0
|
|
79
|
+
print(f" Получено {len(all_posts)} постов...")
|
|
80
|
+
else:
|
|
81
|
+
print(f" Получено {len(all_posts)} постов...")
|
|
52
82
|
|
|
53
83
|
# Проверяем, есть ли ещё страницы
|
|
54
84
|
extra = data.get("extra", {})
|
|
@@ -146,9 +176,9 @@ class BoostyDownloader(BaseDownloader):
|
|
|
146
176
|
try:
|
|
147
177
|
blocks = json.loads(post.content_html)
|
|
148
178
|
except json.JSONDecodeError:
|
|
149
|
-
return
|
|
179
|
+
return ""
|
|
150
180
|
|
|
151
|
-
lines = [
|
|
181
|
+
lines: list[str] = []
|
|
152
182
|
|
|
153
183
|
for block in blocks:
|
|
154
184
|
md = self._block_to_markdown(block, asset_map)
|
|
@@ -58,6 +58,15 @@ class Database:
|
|
|
58
58
|
CREATE INDEX IF NOT EXISTS idx_platform_author
|
|
59
59
|
ON posts(platform, author)
|
|
60
60
|
''')
|
|
61
|
+
conn.execute('''
|
|
62
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
63
|
+
platform TEXT NOT NULL,
|
|
64
|
+
author TEXT NOT NULL,
|
|
65
|
+
is_full_sync INTEGER DEFAULT 0,
|
|
66
|
+
last_sync_at TEXT,
|
|
67
|
+
UNIQUE(platform, author)
|
|
68
|
+
)
|
|
69
|
+
''')
|
|
61
70
|
conn.commit()
|
|
62
71
|
|
|
63
72
|
def close(self):
|
|
@@ -167,3 +176,35 @@ class Database:
|
|
|
167
176
|
tags=row['tags'],
|
|
168
177
|
synced_at=row['synced_at'],
|
|
169
178
|
)
|
|
179
|
+
|
|
180
|
+
def mark_full_sync(self, platform: str, author: str):
|
|
181
|
+
"""Помечает архив как полностью синхронизированный."""
|
|
182
|
+
from datetime import datetime, timezone
|
|
183
|
+
conn = self._get_conn()
|
|
184
|
+
conn.execute('''
|
|
185
|
+
INSERT OR REPLACE INTO sync_state (platform, author, is_full_sync, last_sync_at)
|
|
186
|
+
VALUES (?, ?, 1, ?)
|
|
187
|
+
''', (platform, author, datetime.now(timezone.utc).isoformat()))
|
|
188
|
+
conn.commit()
|
|
189
|
+
|
|
190
|
+
def is_full_sync(self, platform: str, author: str) -> bool:
|
|
191
|
+
"""Проверяет, полностью ли синхронизирован архив."""
|
|
192
|
+
conn = self._get_conn()
|
|
193
|
+
cursor = conn.execute('''
|
|
194
|
+
SELECT is_full_sync FROM sync_state
|
|
195
|
+
WHERE platform = ? AND author = ?
|
|
196
|
+
''', (platform, author))
|
|
197
|
+
row = cursor.fetchone()
|
|
198
|
+
return bool(row and row[0])
|
|
199
|
+
|
|
200
|
+
def update_last_sync(self, platform: str, author: str):
|
|
201
|
+
"""Обновляет время последней синхронизации."""
|
|
202
|
+
from datetime import datetime, timezone
|
|
203
|
+
conn = self._get_conn()
|
|
204
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
205
|
+
conn.execute('''
|
|
206
|
+
INSERT INTO sync_state (platform, author, is_full_sync, last_sync_at)
|
|
207
|
+
VALUES (?, ?, 0, ?)
|
|
208
|
+
ON CONFLICT(platform, author) DO UPDATE SET last_sync_at = ?
|
|
209
|
+
''', (platform, author, now, now))
|
|
210
|
+
conn.commit()
|
|
@@ -98,8 +98,20 @@ class BaseDownloader(ABC):
|
|
|
98
98
|
pass
|
|
99
99
|
|
|
100
100
|
@abstractmethod
|
|
101
|
-
def fetch_posts_list(
|
|
102
|
-
|
|
101
|
+
def fetch_posts_list(
|
|
102
|
+
self,
|
|
103
|
+
existing_ids: set[str] | None = None,
|
|
104
|
+
incremental: bool = False,
|
|
105
|
+
safety_chunks: int = 1
|
|
106
|
+
) -> list[dict]:
|
|
107
|
+
"""
|
|
108
|
+
Получает список постов с API.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
existing_ids: Множество уже загруженных post_id (для инкрементального режима)
|
|
112
|
+
incremental: Включить инкрементальный режим
|
|
113
|
+
safety_chunks: Количество "защитных" чанков перед остановкой
|
|
114
|
+
"""
|
|
103
115
|
pass
|
|
104
116
|
|
|
105
117
|
@abstractmethod
|
|
@@ -119,7 +131,16 @@ class BaseDownloader(ABC):
|
|
|
119
131
|
self._create_index_files()
|
|
120
132
|
|
|
121
133
|
existing_ids = self.db.get_all_post_ids(self.PLATFORM, self.source.author)
|
|
122
|
-
|
|
134
|
+
is_full = self.db.is_full_sync(self.PLATFORM, self.source.author)
|
|
135
|
+
|
|
136
|
+
if is_full:
|
|
137
|
+
# Инкрементальная синхронизация
|
|
138
|
+
print(" Инкрементальный режим...")
|
|
139
|
+
posts = self.fetch_posts_list(existing_ids, incremental=True, safety_chunks=1)
|
|
140
|
+
else:
|
|
141
|
+
# Первый запуск или неполный архив
|
|
142
|
+
print(" Полная загрузка индекса...")
|
|
143
|
+
posts = self.fetch_posts_list()
|
|
123
144
|
|
|
124
145
|
new_posts = [p for p in posts if str(p.get('id', p.get('post_id'))) not in existing_ids]
|
|
125
146
|
print(f" Найдено постов: {len(posts)}, новых: {len(new_posts)}")
|
|
@@ -134,6 +155,13 @@ class BaseDownloader(ABC):
|
|
|
134
155
|
print(f" Фиксим внутренние ссылки...")
|
|
135
156
|
self.fix_internal_links()
|
|
136
157
|
|
|
158
|
+
# Помечаем архив как полный после успешной полной загрузки
|
|
159
|
+
if not is_full:
|
|
160
|
+
print(" ✓ Архив полностью синхронизирован")
|
|
161
|
+
self.db.mark_full_sync(self.PLATFORM, self.source.author)
|
|
162
|
+
|
|
163
|
+
self.db.update_last_sync(self.PLATFORM, self.source.author)
|
|
164
|
+
|
|
137
165
|
def download_single(self, post_id: str):
|
|
138
166
|
"""Скачивает один пост по ID."""
|
|
139
167
|
print(f"[{self.PLATFORM}] Скачивание поста {post_id}...")
|