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.
Files changed (25) hide show
  1. {article_backup-0.2.3 → article_backup-0.3.1}/PKG-INFO +37 -11
  2. {article_backup-0.2.3 → article_backup-0.3.1}/README.md +36 -10
  3. {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/PKG-INFO +37 -11
  4. {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/SOURCES.txt +1 -0
  5. {article_backup-0.2.3 → article_backup-0.3.1}/pyproject.toml +1 -1
  6. {article_backup-0.2.3 → article_backup-0.3.1}/src/boosty.py +35 -5
  7. {article_backup-0.2.3 → article_backup-0.3.1}/src/database.py +41 -0
  8. {article_backup-0.2.3 → article_backup-0.3.1}/src/downloader.py +31 -3
  9. article_backup-0.3.1/src/sponsr.py +655 -0
  10. {article_backup-0.2.3 → article_backup-0.3.1}/tests/test_asset_dedup.py +6 -1
  11. article_backup-0.3.1/tests/test_incremental_sync.py +216 -0
  12. article_backup-0.3.1/tests/test_sponsr_normalize.py +359 -0
  13. article_backup-0.2.3/src/sponsr.py +0 -349
  14. article_backup-0.2.3/tests/test_sponsr_normalize.py +0 -153
  15. {article_backup-0.2.3 → article_backup-0.3.1}/LICENSE +0 -0
  16. {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/dependency_links.txt +0 -0
  17. {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/entry_points.txt +0 -0
  18. {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/requires.txt +0 -0
  19. {article_backup-0.2.3 → article_backup-0.3.1}/article_backup.egg-info/top_level.txt +0 -0
  20. {article_backup-0.2.3 → article_backup-0.3.1}/backup.py +0 -0
  21. {article_backup-0.2.3 → article_backup-0.3.1}/setup.cfg +0 -0
  22. {article_backup-0.2.3 → article_backup-0.3.1}/src/__init__.py +0 -0
  23. {article_backup-0.2.3 → article_backup-0.3.1}/src/config.py +0 -0
  24. {article_backup-0.2.3 → article_backup-0.3.1}/src/utils.py +0 -0
  25. {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.2.3
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
- - Конвертация в 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-индекс для быстрого поиска
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
- - Конвертация в 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-индекс для быстрого поиска
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.2.3
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
- - Конвертация в 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-индекс для быстрого поиска
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
  ### Тесты
@@ -16,5 +16,6 @@ src/downloader.py
16
16
  src/sponsr.py
17
17
  src/utils.py
18
18
  tests/test_asset_dedup.py
19
+ tests/test_incremental_sync.py
19
20
  tests/test_sponsr_normalize.py
20
21
  tests/test_sponsr_tags.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "article-backup"
3
- version = "0.2.3"
3
+ version = "0.3.1"
4
4
  description = "Локальный бэкап статей с Sponsr.ru и Boosty.to в Markdown с Hugo-интеграцией"
5
5
  readme = "README.md"
6
6
  license = {text = "Apache-2.0"}
@@ -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(self) -> list[dict]:
32
- """Получает список всех постов через API."""
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
- print(f" Получено {len(all_posts)} постов...")
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 f"# {post.title}\n\n"
179
+ return ""
150
180
 
151
- lines = [f"# {post.title}\n"]
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(self) -> list[dict]:
102
- """Получает список постов с API."""
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
- posts = self.fetch_posts_list()
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}...")