Add server sync option to enhanced library write-tags flow
This commit is contained in:
parent
729f42f1a4
commit
cc4502e5f8
3 changed files with 181 additions and 12 deletions
119
web_server.py
119
web_server.py
|
|
@ -8769,6 +8769,17 @@ def get_artist_enhanced_detail(artist_id):
|
||||||
if album.get('thumb_url'):
|
if album.get('thumb_url'):
|
||||||
album['thumb_url'] = fix_artist_image_url(album['thumb_url'])
|
album['thumb_url'] = fix_artist_image_url(album['thumb_url'])
|
||||||
|
|
||||||
|
# Include server type for sync option
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
server_connected = False
|
||||||
|
if active_server == 'plex':
|
||||||
|
server_connected = plex_client.is_connected()
|
||||||
|
elif active_server == 'jellyfin':
|
||||||
|
server_connected = jellyfin_client.is_connected()
|
||||||
|
elif active_server == 'navidrome':
|
||||||
|
server_connected = navidrome_client.is_connected()
|
||||||
|
result['server_type'] = active_server if server_connected else None
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
@ -8902,6 +8913,16 @@ def get_track_tag_preview(track_id):
|
||||||
diff = build_tag_diff(file_tags, db_data)
|
diff = build_tag_diff(file_tags, db_data)
|
||||||
has_changes = any(d['changed'] for d in diff)
|
has_changes = any(d['changed'] for d in diff)
|
||||||
|
|
||||||
|
# Include server type so frontend can offer server sync option
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
server_connected = False
|
||||||
|
if active_server == 'plex':
|
||||||
|
server_connected = plex_client.is_connected()
|
||||||
|
elif active_server == 'jellyfin':
|
||||||
|
server_connected = jellyfin_client.is_connected()
|
||||||
|
elif active_server == 'navidrome':
|
||||||
|
server_connected = navidrome_client.is_connected()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"file_path": resolved_path,
|
"file_path": resolved_path,
|
||||||
|
|
@ -8909,6 +8930,7 @@ def get_track_tag_preview(track_id):
|
||||||
"db_data": db_data,
|
"db_data": db_data,
|
||||||
"diff": diff,
|
"diff": diff,
|
||||||
"has_changes": has_changes,
|
"has_changes": has_changes,
|
||||||
|
"server_type": active_server if server_connected else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -8982,6 +9004,13 @@ def write_track_tags(track_id):
|
||||||
with file_lock:
|
with file_lock:
|
||||||
result = write_tags_to_file(resolved_path, db_data, embed_cover=embed_cover, cover_url=cover_url)
|
result = write_tags_to_file(resolved_path, db_data, embed_cover=embed_cover, cover_url=cover_url)
|
||||||
|
|
||||||
|
# Sync to media server if requested and write succeeded
|
||||||
|
sync_result = None
|
||||||
|
if result.get('success') and data.get('sync_to_server'):
|
||||||
|
server_type = config_manager.get_active_media_server()
|
||||||
|
sync_result = _sync_tracks_to_server([track_data], server_type)
|
||||||
|
result['server_sync'] = sync_result
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -8997,6 +9026,10 @@ _write_tags_batch_state = {
|
||||||
'failed': 0,
|
'failed': 0,
|
||||||
'current_track': '',
|
'current_track': '',
|
||||||
'errors': [],
|
'errors': [],
|
||||||
|
'sync_phase': None, # None | 'syncing' | 'done'
|
||||||
|
'sync_server': None,
|
||||||
|
'sync_synced': 0,
|
||||||
|
'sync_failed': 0,
|
||||||
}
|
}
|
||||||
_write_tags_batch_lock = threading.Lock()
|
_write_tags_batch_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
@ -9033,6 +9066,8 @@ def write_tracks_tags_batch():
|
||||||
|
|
||||||
rows = [dict(r) for r in cursor.fetchall()]
|
rows = [dict(r) for r in cursor.fetchall()]
|
||||||
|
|
||||||
|
sync_to_server = data.get('sync_to_server', False)
|
||||||
|
|
||||||
# Initialize state
|
# Initialize state
|
||||||
with _write_tags_batch_lock:
|
with _write_tags_batch_lock:
|
||||||
_write_tags_batch_state.update({
|
_write_tags_batch_state.update({
|
||||||
|
|
@ -9043,6 +9078,10 @@ def write_tracks_tags_batch():
|
||||||
'failed': 0,
|
'failed': 0,
|
||||||
'current_track': '',
|
'current_track': '',
|
||||||
'errors': [],
|
'errors': [],
|
||||||
|
'sync_phase': None,
|
||||||
|
'sync_server': None,
|
||||||
|
'sync_synced': 0,
|
||||||
|
'sync_failed': 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Count missing DB rows
|
# Count missing DB rows
|
||||||
|
|
@ -9060,6 +9099,8 @@ def write_tracks_tags_batch():
|
||||||
try:
|
try:
|
||||||
from core.tag_writer import write_tags_to_file, download_cover_art
|
from core.tag_writer import write_tags_to_file, download_cover_art
|
||||||
|
|
||||||
|
written_tracks = [] # Track dicts that were successfully written (for server sync)
|
||||||
|
|
||||||
# Pre-download cover art once per unique album URL
|
# Pre-download cover art once per unique album URL
|
||||||
cover_cache = {} # url → (bytes, mime) or None
|
cover_cache = {} # url → (bytes, mime) or None
|
||||||
if embed_cover:
|
if embed_cover:
|
||||||
|
|
@ -9129,6 +9170,7 @@ def write_tracks_tags_batch():
|
||||||
_write_tags_batch_state['processed'] += 1
|
_write_tags_batch_state['processed'] += 1
|
||||||
if write_result.get('success'):
|
if write_result.get('success'):
|
||||||
_write_tags_batch_state['written'] += 1
|
_write_tags_batch_state['written'] += 1
|
||||||
|
written_tracks.append(track_data)
|
||||||
else:
|
else:
|
||||||
_write_tags_batch_state['failed'] += 1
|
_write_tags_batch_state['failed'] += 1
|
||||||
_write_tags_batch_state['errors'].append({
|
_write_tags_batch_state['errors'].append({
|
||||||
|
|
@ -9136,6 +9178,21 @@ def write_tracks_tags_batch():
|
||||||
'error': write_result.get('error', 'Unknown')
|
'error': write_result.get('error', 'Unknown')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Server sync phase
|
||||||
|
if sync_to_server and written_tracks:
|
||||||
|
server_type = config_manager.get_active_media_server()
|
||||||
|
with _write_tags_batch_lock:
|
||||||
|
_write_tags_batch_state['sync_phase'] = 'syncing'
|
||||||
|
_write_tags_batch_state['sync_server'] = server_type
|
||||||
|
_write_tags_batch_state['current_track'] = f'Syncing to {server_type.title()}...'
|
||||||
|
|
||||||
|
sync_result = _sync_tracks_to_server(written_tracks, server_type)
|
||||||
|
|
||||||
|
with _write_tags_batch_lock:
|
||||||
|
_write_tags_batch_state['sync_phase'] = 'done'
|
||||||
|
_write_tags_batch_state['sync_synced'] = sync_result['synced']
|
||||||
|
_write_tags_batch_state['sync_failed'] = sync_result['failed']
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Batch write tags background error: {e}")
|
logger.error(f"Batch write tags background error: {e}")
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -9164,6 +9221,68 @@ def get_write_tags_batch_status():
|
||||||
return jsonify(state)
|
return jsonify(state)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_tracks_to_server(track_rows, server_type):
|
||||||
|
"""Sync metadata for tracks to the active media server after writing file tags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
track_rows: list of track dicts (must include 'id', 'title', 'artist_name', 'album_title', 'year', 'server_source')
|
||||||
|
server_type: 'plex', 'jellyfin', or 'navidrome'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with 'synced', 'failed', 'skipped' counts and 'errors' list
|
||||||
|
"""
|
||||||
|
result = {'synced': 0, 'failed': 0, 'skipped': 0, 'errors': []}
|
||||||
|
|
||||||
|
if server_type == 'navidrome':
|
||||||
|
# Navidrome auto-detects file tag changes, no action needed
|
||||||
|
result['synced'] = len(track_rows)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if server_type == 'plex':
|
||||||
|
for track_data in track_rows:
|
||||||
|
# Only sync tracks that came from this server
|
||||||
|
if track_data.get('server_source') and track_data['server_source'] != 'plex':
|
||||||
|
result['skipped'] += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
metadata = {}
|
||||||
|
if track_data.get('title'):
|
||||||
|
metadata['title'] = track_data['title']
|
||||||
|
if track_data.get('artist_name'):
|
||||||
|
metadata['artist'] = track_data['artist_name']
|
||||||
|
if track_data.get('album_title'):
|
||||||
|
metadata['album'] = track_data['album_title']
|
||||||
|
if track_data.get('year'):
|
||||||
|
metadata['year'] = track_data['year']
|
||||||
|
if metadata:
|
||||||
|
success = plex_client.update_track_metadata(str(track_data['id']), metadata)
|
||||||
|
if success:
|
||||||
|
result['synced'] += 1
|
||||||
|
else:
|
||||||
|
result['failed'] += 1
|
||||||
|
result['errors'].append({'track_id': track_data['id'], 'error': 'Plex update returned false'})
|
||||||
|
else:
|
||||||
|
result['skipped'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
result['failed'] += 1
|
||||||
|
result['errors'].append({'track_id': track_data['id'], 'error': str(e)})
|
||||||
|
|
||||||
|
elif server_type == 'jellyfin':
|
||||||
|
# Jellyfin: just trigger a library scan once after all file writes
|
||||||
|
try:
|
||||||
|
success = jellyfin_client.trigger_library_scan()
|
||||||
|
if success:
|
||||||
|
result['synced'] = len(track_rows)
|
||||||
|
else:
|
||||||
|
result['failed'] = len(track_rows)
|
||||||
|
result['errors'].append({'error': 'Jellyfin library scan failed'})
|
||||||
|
except Exception as e:
|
||||||
|
result['failed'] = len(track_rows)
|
||||||
|
result['errors'].append({'error': f'Jellyfin scan error: {e}'})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _resolve_library_file_path(file_path):
|
def _resolve_library_file_path(file_path):
|
||||||
"""Resolve a library file path to an actual file on disk."""
|
"""Resolve a library file path to an actual file on disk."""
|
||||||
if not file_path:
|
if not file_path:
|
||||||
|
|
|
||||||
|
|
@ -2367,6 +2367,10 @@
|
||||||
<input type="checkbox" id="tag-preview-embed-cover" checked>
|
<input type="checkbox" id="tag-preview-embed-cover" checked>
|
||||||
Embed cover art
|
Embed cover art
|
||||||
</label>
|
</label>
|
||||||
|
<label class="tag-preview-cover-label hidden" id="tag-preview-sync-label">
|
||||||
|
<input type="checkbox" id="tag-preview-sync-server" checked>
|
||||||
|
<span id="tag-preview-sync-text">Sync to server</span>
|
||||||
|
</label>
|
||||||
<div class="tag-preview-footer-actions">
|
<div class="tag-preview-footer-actions">
|
||||||
<button class="enhanced-bulk-btn secondary" onclick="closeTagPreviewModal()">Cancel</button>
|
<button class="enhanced-bulk-btn secondary" onclick="closeTagPreviewModal()">Cancel</button>
|
||||||
<button class="enhanced-bulk-btn primary" id="tag-preview-write-btn" onclick="executeWriteTags()">Write Tags</button>
|
<button class="enhanced-bulk-btn primary" id="tag-preview-write-btn" onclick="executeWriteTags()">Write Tags</button>
|
||||||
|
|
|
||||||
|
|
@ -33140,6 +33140,8 @@ async function loadEnhancedViewData(artistId) {
|
||||||
artistDetailPageState.expandedAlbums = new Set();
|
artistDetailPageState.expandedAlbums = new Set();
|
||||||
artistDetailPageState.selectedTracks = new Set();
|
artistDetailPageState.selectedTracks = new Set();
|
||||||
artistDetailPageState.enhancedTrackSort = {};
|
artistDetailPageState.enhancedTrackSort = {};
|
||||||
|
artistDetailPageState.serverType = data.server_type || null;
|
||||||
|
_tagPreviewServerType = data.server_type || null;
|
||||||
renderEnhancedView();
|
renderEnhancedView();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -34902,9 +34904,11 @@ document.addEventListener('click', (e) => {
|
||||||
// ---- Write Tags to File ----
|
// ---- Write Tags to File ----
|
||||||
|
|
||||||
let _tagPreviewTrackId = null;
|
let _tagPreviewTrackId = null;
|
||||||
|
let _tagPreviewServerType = null;
|
||||||
|
|
||||||
async function showTagPreview(trackId) {
|
async function showTagPreview(trackId) {
|
||||||
_tagPreviewTrackId = trackId;
|
_tagPreviewTrackId = trackId;
|
||||||
|
_tagPreviewServerType = null;
|
||||||
const overlay = document.getElementById('tag-preview-overlay');
|
const overlay = document.getElementById('tag-preview-overlay');
|
||||||
const body = document.getElementById('tag-preview-body');
|
const body = document.getElementById('tag-preview-body');
|
||||||
const title = document.getElementById('tag-preview-title');
|
const title = document.getElementById('tag-preview-title');
|
||||||
|
|
@ -34914,6 +34918,10 @@ async function showTagPreview(trackId) {
|
||||||
body.innerHTML = '<div class="tag-preview-loading">Loading tag comparison...</div>';
|
body.innerHTML = '<div class="tag-preview-loading">Loading tag comparison...</div>';
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Hide sync checkbox until we know server type
|
||||||
|
const syncLabel = document.getElementById('tag-preview-sync-label');
|
||||||
|
if (syncLabel) syncLabel.classList.add('hidden');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/library/track/${trackId}/tag-preview`);
|
const response = await fetch(`/api/library/track/${trackId}/tag-preview`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
@ -34925,6 +34933,14 @@ async function showTagPreview(trackId) {
|
||||||
const diff = result.diff || [];
|
const diff = result.diff || [];
|
||||||
const hasChanges = result.has_changes;
|
const hasChanges = result.has_changes;
|
||||||
|
|
||||||
|
// Show server sync checkbox if a server is connected (not navidrome — it auto-detects)
|
||||||
|
_tagPreviewServerType = result.server_type || null;
|
||||||
|
if (syncLabel && _tagPreviewServerType && _tagPreviewServerType !== 'navidrome') {
|
||||||
|
const syncText = document.getElementById('tag-preview-sync-text');
|
||||||
|
if (syncText) syncText.textContent = `Sync to ${_tagPreviewServerType === 'plex' ? 'Plex' : 'Jellyfin'}`;
|
||||||
|
syncLabel.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
let html = '<table class="tag-preview-table"><thead><tr>';
|
let html = '<table class="tag-preview-table"><thead><tr>';
|
||||||
html += '<th>Field</th><th>Current File Tag</th><th></th><th>DB Value</th>';
|
html += '<th>Field</th><th>Current File Tag</th><th></th><th>DB Value</th>';
|
||||||
html += '</tr></thead><tbody>';
|
html += '</tr></thead><tbody>';
|
||||||
|
|
@ -34974,18 +34990,25 @@ async function executeWriteTags() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedCover = document.getElementById('tag-preview-embed-cover')?.checked ?? true;
|
const embedCover = document.getElementById('tag-preview-embed-cover')?.checked ?? true;
|
||||||
|
const syncToServer = document.getElementById('tag-preview-sync-server')?.checked && _tagPreviewServerType && _tagPreviewServerType !== 'navidrome';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/library/track/${_tagPreviewTrackId}/write-tags`, {
|
const response = await fetch(`/api/library/track/${_tagPreviewTrackId}/write-tags`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ embed_cover: embedCover })
|
body: JSON.stringify({ embed_cover: embedCover, sync_to_server: syncToServer })
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!result.success) throw new Error(result.error);
|
if (!result.success) throw new Error(result.error);
|
||||||
|
|
||||||
const fieldCount = (result.written_fields || []).length;
|
const fieldCount = (result.written_fields || []).length;
|
||||||
showToast(`Tags written successfully (${fieldCount} fields)`, 'success');
|
let msg = `Tags written successfully (${fieldCount} fields)`;
|
||||||
|
if (result.server_sync) {
|
||||||
|
const ss = result.server_sync;
|
||||||
|
if (ss.synced > 0) msg += ` — synced to ${_tagPreviewServerType === 'plex' ? 'Plex' : 'Jellyfin'}`;
|
||||||
|
else if (ss.failed > 0) msg += ` — server sync failed`;
|
||||||
|
}
|
||||||
|
showToast(msg, 'success');
|
||||||
closeTagPreviewModal();
|
closeTagPreviewModal();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -35008,24 +35031,34 @@ async function writeAlbumTags(albumId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirm(`Write DB metadata to file tags for all ${tracks.length} tracks in "${album.title}"?`)) return;
|
const serverType = artistDetailPageState.serverType;
|
||||||
await _startBatchWriteTags(tracks.map(t => t.id), true);
|
const canSync = serverType && serverType !== 'navidrome';
|
||||||
|
const serverLabel = serverType === 'plex' ? 'Plex' : serverType === 'jellyfin' ? 'Jellyfin' : '';
|
||||||
|
let msg = `Write DB metadata to file tags for all ${tracks.length} tracks in "${album.title}"?`;
|
||||||
|
if (canSync) msg += `\n\nThis will also sync changes to ${serverLabel}.`;
|
||||||
|
if (!confirm(msg)) return;
|
||||||
|
await _startBatchWriteTags(tracks.map(t => t.id), true, canSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function batchWriteTagsSelected() {
|
async function batchWriteTagsSelected() {
|
||||||
const trackIds = Array.from(artistDetailPageState.selectedTracks);
|
const trackIds = Array.from(artistDetailPageState.selectedTracks);
|
||||||
if (trackIds.length === 0) return;
|
if (trackIds.length === 0) return;
|
||||||
|
|
||||||
if (!confirm(`Write DB metadata to file tags for ${trackIds.length} selected track(s)?`)) return;
|
const serverType = artistDetailPageState.serverType;
|
||||||
await _startBatchWriteTags(trackIds, true);
|
const canSync = serverType && serverType !== 'navidrome';
|
||||||
|
const serverLabel = serverType === 'plex' ? 'Plex' : serverType === 'jellyfin' ? 'Jellyfin' : '';
|
||||||
|
let msg = `Write DB metadata to file tags for ${trackIds.length} selected track(s)?`;
|
||||||
|
if (canSync) msg += `\n\nThis will also sync changes to ${serverLabel}.`;
|
||||||
|
if (!confirm(msg)) return;
|
||||||
|
await _startBatchWriteTags(trackIds, true, canSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _startBatchWriteTags(trackIds, embedCover) {
|
async function _startBatchWriteTags(trackIds, embedCover, syncToServer = false) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/library/tracks/write-tags-batch', {
|
const response = await fetch('/api/library/tracks/write-tags-batch', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ track_ids: trackIds, embed_cover: embedCover })
|
body: JSON.stringify({ track_ids: trackIds, embed_cover: embedCover, sync_to_server: syncToServer })
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!result.success) throw new Error(result.error);
|
if (!result.success) throw new Error(result.error);
|
||||||
|
|
@ -35049,12 +35082,25 @@ function _pollBatchWriteTagsStatus() {
|
||||||
const state = await response.json();
|
const state = await response.json();
|
||||||
|
|
||||||
if (state.status === 'running') {
|
if (state.status === 'running') {
|
||||||
const pct = state.total > 0 ? Math.round(state.processed / state.total * 100) : 0;
|
if (state.sync_phase === 'syncing') {
|
||||||
showToast(`Writing tags: ${state.processed}/${state.total} (${pct}%) — ${state.current_track}`, 'info');
|
const serverName = state.sync_server === 'plex' ? 'Plex' : state.sync_server === 'jellyfin' ? 'Jellyfin' : state.sync_server;
|
||||||
|
showToast(`Syncing to ${serverName}...`, 'info');
|
||||||
|
} else {
|
||||||
|
const pct = state.total > 0 ? Math.round(state.processed / state.total * 100) : 0;
|
||||||
|
showToast(`Writing tags: ${state.processed}/${state.total} (${pct}%) — ${state.current_track}`, 'info');
|
||||||
|
}
|
||||||
_batchWriteTagsPollTimer = setTimeout(poll, 1000);
|
_batchWriteTagsPollTimer = setTimeout(poll, 1000);
|
||||||
} else if (state.status === 'done') {
|
} else if (state.status === 'done') {
|
||||||
const msg = `Tags written: ${state.written} succeeded, ${state.failed} failed`;
|
let msg = `Tags written: ${state.written} succeeded, ${state.failed} failed`;
|
||||||
showToast(msg, state.failed > 0 ? 'warning' : 'success');
|
if (state.sync_phase === 'done') {
|
||||||
|
const serverName = state.sync_server === 'plex' ? 'Plex' : state.sync_server === 'jellyfin' ? 'Jellyfin' : state.sync_server;
|
||||||
|
if (state.sync_synced > 0 && state.sync_failed === 0) {
|
||||||
|
msg += ` — synced to ${serverName}`;
|
||||||
|
} else if (state.sync_failed > 0) {
|
||||||
|
msg += ` — ${serverName} sync: ${state.sync_synced} synced, ${state.sync_failed} failed`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(msg, state.failed > 0 || state.sync_failed > 0 ? 'warning' : 'success');
|
||||||
_batchWriteTagsPollTimer = null;
|
_batchWriteTagsPollTimer = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue