syncfm-core 0.2.0__tar.gz → 0.2.2__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.
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/PKG-INFO +1 -1
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/pyproject.toml +1 -1
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/pyproject.toml.orig +1 -1
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/dashboard.py +35 -6
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/sync_progress.py +8 -2
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/networks/pylast_adapter.py +41 -17
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/metadata_service.py +8 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/submission_service.py +8 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/LICENSE +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/README.md +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/application.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/app.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/commands/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/commands/common.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/commands/discard.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/commands/profile.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/commands/resume.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/commands/status.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/commands/sync.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/configuration.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/context.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/presenters.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/cli/verification.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/exceptions.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/network_config.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/retry_policy.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/scrobble.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/sync_queue_item.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/sync_request.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/sync_result.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/sync_session.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/sync_status.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/models/sync_verification.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/networks/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/networks/factory.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/networks/gnufm.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/networks/pylast_mapper.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/factory.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/keyring_store.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/models.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/paths.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/protocols.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/resolver.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/service.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/profiles/toml_store.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/protocols/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/protocols/network.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/protocols/repositories.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/py.typed +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/repositories/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/repositories/database.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/repositories/queue_repository.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/repositories/schema.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/repositories/session_repository.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/repositories/snapshot_repository.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/__init__.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/comparison_service.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/range_resolver.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/recovery_service.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/retry_service.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/snapshot_service.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/sync_service.py +0 -0
- {syncfm_core-0.2.0 → syncfm_core-0.2.2}/src/syncfm_core/services/verification_service.py +0 -0
|
@@ -64,6 +64,7 @@ class _RecentScrobble:
|
|
|
64
64
|
scrobble: Scrobble
|
|
65
65
|
phase: SyncPhase
|
|
66
66
|
completed: bool
|
|
67
|
+
failed: bool
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
class RichProgressPresenter:
|
|
@@ -124,7 +125,7 @@ class RichProgressPresenter:
|
|
|
124
125
|
self._task_id: TaskID | None = None
|
|
125
126
|
self._rendered_phase: SyncPhase | None = None
|
|
126
127
|
self._live: Live | None = None
|
|
127
|
-
self._last_plain_update: tuple[SyncPhase, int, int] | None = None
|
|
128
|
+
self._last_plain_update: tuple[SyncPhase, int, int, bool] | None = None
|
|
128
129
|
|
|
129
130
|
def configure(self, *, source_label: str, target_label: str) -> None:
|
|
130
131
|
"""Update the source and target labels before rendering starts."""
|
|
@@ -364,11 +365,15 @@ class RichProgressPresenter:
|
|
|
364
365
|
scrobble=scrobble,
|
|
365
366
|
phase=progress.phase,
|
|
366
367
|
completed=progress.current_scrobble_completed,
|
|
368
|
+
failed=progress.current_scrobble_failed,
|
|
367
369
|
)
|
|
368
370
|
|
|
369
371
|
@staticmethod
|
|
370
372
|
def _recent_status(recent: _RecentScrobble) -> tuple[str, str]:
|
|
371
373
|
"""Return the label and Rich style for a recent-scrobble row."""
|
|
374
|
+
if recent.failed:
|
|
375
|
+
return "✗ FAILED", "bold red"
|
|
376
|
+
|
|
372
377
|
if recent.completed:
|
|
373
378
|
if recent.phase is SyncPhase.ENRICHING:
|
|
374
379
|
return "✓ READY", "bold green"
|
|
@@ -394,24 +399,43 @@ class RichProgressPresenter:
|
|
|
394
399
|
|
|
395
400
|
def _render_plain_update(self, progress: SyncProgress) -> None:
|
|
396
401
|
"""Print concise progress when output is not an interactive terminal."""
|
|
397
|
-
snapshot = (
|
|
402
|
+
snapshot = (
|
|
403
|
+
progress.phase,
|
|
404
|
+
progress.completed,
|
|
405
|
+
progress.total,
|
|
406
|
+
progress.current_scrobble_failed,
|
|
407
|
+
)
|
|
398
408
|
phase_changed = (
|
|
399
409
|
self._last_plain_update is None
|
|
400
410
|
or self._last_plain_update[0] is not progress.phase
|
|
401
411
|
)
|
|
402
|
-
phase_finished =
|
|
412
|
+
phase_finished = 0 < progress.total == progress.completed
|
|
403
413
|
|
|
404
|
-
if
|
|
414
|
+
if (
|
|
415
|
+
not phase_changed
|
|
416
|
+
and not phase_finished
|
|
417
|
+
and not progress.current_scrobble_failed
|
|
418
|
+
):
|
|
405
419
|
return
|
|
406
420
|
|
|
407
421
|
if snapshot == self._last_plain_update:
|
|
408
422
|
return
|
|
409
423
|
|
|
410
424
|
message = Text()
|
|
411
|
-
|
|
412
|
-
|
|
425
|
+
|
|
426
|
+
if progress.current_scrobble_failed:
|
|
427
|
+
message.append("✗ ", style="bold red")
|
|
428
|
+
if progress.phase is SyncPhase.ENRICHING:
|
|
429
|
+
message.append("Metadata lookup failed", style="bold red")
|
|
430
|
+
else:
|
|
431
|
+
message.append("Scrobble submission failed", style="bold red")
|
|
432
|
+
else:
|
|
433
|
+
message.append("● ", style=self._phase_color())
|
|
434
|
+
message.append(_PHASE_LABELS[progress.phase])
|
|
435
|
+
|
|
413
436
|
if progress.total > 0:
|
|
414
437
|
message.append(f" {progress.completed}/{progress.total}", style="dim")
|
|
438
|
+
|
|
415
439
|
self._console.print(message)
|
|
416
440
|
self._last_plain_update = snapshot
|
|
417
441
|
|
|
@@ -425,6 +449,8 @@ class RichProgressPresenter:
|
|
|
425
449
|
|
|
426
450
|
def _phase_color(self) -> str:
|
|
427
451
|
"""Return a consistent color for the current phase."""
|
|
452
|
+
if self._latest.current_scrobble_failed:
|
|
453
|
+
return "red"
|
|
428
454
|
if self._latest.phase is SyncPhase.COMPLETED:
|
|
429
455
|
return "green"
|
|
430
456
|
if self._latest.phase is SyncPhase.FAILED:
|
|
@@ -433,6 +459,9 @@ class RichProgressPresenter:
|
|
|
433
459
|
|
|
434
460
|
def _activity_badge(self) -> str:
|
|
435
461
|
"""Return a short uppercase status for the current activity."""
|
|
462
|
+
if self._latest.current_scrobble_failed:
|
|
463
|
+
return "FAILED"
|
|
464
|
+
|
|
436
465
|
if self._latest.current_scrobble_completed:
|
|
437
466
|
if self._latest.phase is SyncPhase.ENRICHING:
|
|
438
467
|
return "READY"
|
|
@@ -16,6 +16,7 @@ class SyncProgress:
|
|
|
16
16
|
total: int
|
|
17
17
|
current_scrobble: Scrobble | None = None
|
|
18
18
|
current_scrobble_completed: bool = False
|
|
19
|
+
current_scrobble_failed: bool = False
|
|
19
20
|
|
|
20
21
|
def __post_init__(self) -> None:
|
|
21
22
|
"""Validate progress values."""
|
|
@@ -25,10 +26,15 @@ class SyncProgress:
|
|
|
25
26
|
raise ValueError("total must be non-negative")
|
|
26
27
|
if self.completed > self.total:
|
|
27
28
|
raise ValueError("completed must not exceed total")
|
|
28
|
-
if
|
|
29
|
+
if (
|
|
30
|
+
self.current_scrobble_completed or self.current_scrobble_failed
|
|
31
|
+
) and self.current_scrobble is None:
|
|
29
32
|
raise ValueError(
|
|
30
|
-
"Completed current-scrobble state requires a current scrobble."
|
|
33
|
+
"Completed or failed current-scrobble state requires a current scrobble."
|
|
31
34
|
)
|
|
32
35
|
|
|
36
|
+
if self.current_scrobble_completed and self.current_scrobble_failed:
|
|
37
|
+
raise ValueError("A current scrobble cannot be both completed and failed.")
|
|
38
|
+
|
|
33
39
|
|
|
34
40
|
type ProgressCallback = Callable[[SyncProgress], None]
|
|
@@ -133,34 +133,58 @@ class PyLastNetworkAdapter:
|
|
|
133
133
|
end_timestamp: int | None = None,
|
|
134
134
|
limit: int | None = None,
|
|
135
135
|
) -> list[Scrobble]:
|
|
136
|
-
"""Return scrobbles
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
start_timestamp
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
"""Return scrobbles within the inclusive timestamp range."""
|
|
137
|
+
api_start_timestamp = (
|
|
138
|
+
start_timestamp - 1
|
|
139
|
+
if start_timestamp is not None and start_timestamp > 0
|
|
140
|
+
else None
|
|
141
|
+
)
|
|
142
|
+
api_end_timestamp = (
|
|
143
|
+
end_timestamp + 1
|
|
144
|
+
if end_timestamp is not None
|
|
145
|
+
else None
|
|
146
|
+
)
|
|
142
147
|
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
api_limit = (
|
|
149
|
+
None
|
|
150
|
+
if limit is not None
|
|
151
|
+
and (start_timestamp is not None or end_timestamp is not None)
|
|
152
|
+
else limit
|
|
153
|
+
)
|
|
145
154
|
|
|
146
|
-
Raises:
|
|
147
|
-
NetworkError: If pylast cannot retrieve playback history.
|
|
148
|
-
InvalidNetworkResponseError: If returned playback data is malformed.
|
|
149
|
-
"""
|
|
150
155
|
try:
|
|
151
156
|
user = self._network.get_user(self._username)
|
|
152
157
|
played_tracks = user.get_recent_tracks(
|
|
153
|
-
limit=
|
|
154
|
-
time_from=
|
|
155
|
-
time_to=
|
|
158
|
+
limit=api_limit,
|
|
159
|
+
time_from=api_start_timestamp,
|
|
160
|
+
time_to=api_end_timestamp,
|
|
156
161
|
stream=False,
|
|
157
162
|
)
|
|
158
|
-
|
|
159
|
-
self._mapper.to_scrobble(played_track)
|
|
163
|
+
scrobbles = [
|
|
164
|
+
self._mapper.to_scrobble(played_track)
|
|
165
|
+
for played_track in played_tracks
|
|
160
166
|
]
|
|
161
167
|
except pylast.PyLastError as error:
|
|
162
168
|
raise self.translate_error(error) from error
|
|
163
169
|
|
|
170
|
+
filtered_scrobbles = [
|
|
171
|
+
scrobble
|
|
172
|
+
for scrobble in scrobbles
|
|
173
|
+
if (
|
|
174
|
+
start_timestamp is None
|
|
175
|
+
or scrobble.timestamp >= start_timestamp
|
|
176
|
+
)
|
|
177
|
+
and (
|
|
178
|
+
end_timestamp is None
|
|
179
|
+
or scrobble.timestamp <= end_timestamp
|
|
180
|
+
)
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
if limit is not None:
|
|
184
|
+
return filtered_scrobbles[:limit]
|
|
185
|
+
|
|
186
|
+
return filtered_scrobbles
|
|
187
|
+
|
|
164
188
|
def get_latest_scrobble_timestamp(self) -> int | None:
|
|
165
189
|
"""Return the latest scrobble timestamp, if one exists."""
|
|
166
190
|
scrobbles = self.get_scrobbles(limit=1)
|
|
@@ -83,6 +83,12 @@ class MetadataService:
|
|
|
83
83
|
item.id,
|
|
84
84
|
self._error_message(error),
|
|
85
85
|
)
|
|
86
|
+
self._report_progress(
|
|
87
|
+
completed,
|
|
88
|
+
total,
|
|
89
|
+
item.scrobble,
|
|
90
|
+
current_scrobble_failed=True,
|
|
91
|
+
)
|
|
86
92
|
raise
|
|
87
93
|
|
|
88
94
|
self._queue_repository.update_metadata(item.id, metadata)
|
|
@@ -103,6 +109,7 @@ class MetadataService:
|
|
|
103
109
|
current_scrobble: Scrobble | None = None,
|
|
104
110
|
*,
|
|
105
111
|
current_scrobble_completed: bool = False,
|
|
112
|
+
current_scrobble_failed: bool = False,
|
|
106
113
|
) -> None:
|
|
107
114
|
"""Report metadata progress when a callback is configured."""
|
|
108
115
|
if self._progress_callback is not None:
|
|
@@ -113,6 +120,7 @@ class MetadataService:
|
|
|
113
120
|
total=total,
|
|
114
121
|
current_scrobble=current_scrobble,
|
|
115
122
|
current_scrobble_completed=current_scrobble_completed,
|
|
123
|
+
current_scrobble_failed=current_scrobble_failed,
|
|
116
124
|
)
|
|
117
125
|
)
|
|
118
126
|
|
|
@@ -82,6 +82,12 @@ class SubmissionService:
|
|
|
82
82
|
item.id,
|
|
83
83
|
self._error_message(error),
|
|
84
84
|
)
|
|
85
|
+
self._report_progress(
|
|
86
|
+
completed,
|
|
87
|
+
total,
|
|
88
|
+
item.scrobble,
|
|
89
|
+
current_scrobble_failed=True,
|
|
90
|
+
)
|
|
85
91
|
raise
|
|
86
92
|
|
|
87
93
|
self._queue_repository.update_status(
|
|
@@ -101,6 +107,7 @@ class SubmissionService:
|
|
|
101
107
|
current_scrobble: Scrobble | None = None,
|
|
102
108
|
*,
|
|
103
109
|
current_scrobble_completed: bool = False,
|
|
110
|
+
current_scrobble_failed: bool = False,
|
|
104
111
|
) -> None:
|
|
105
112
|
"""Report submission progress when a callback is configured."""
|
|
106
113
|
if self._progress_callback is not None:
|
|
@@ -111,6 +118,7 @@ class SubmissionService:
|
|
|
111
118
|
total=total,
|
|
112
119
|
current_scrobble=current_scrobble,
|
|
113
120
|
current_scrobble_completed=current_scrobble_completed,
|
|
121
|
+
current_scrobble_failed=current_scrobble_failed,
|
|
114
122
|
)
|
|
115
123
|
)
|
|
116
124
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|