Add favorite sync to play_state_sync — union semantics, music first
Favorites on any server propagate to all others; never unmarks. Covers MusicArtist, MusicAlbum, Movie, Series (Audio tracks future). Provider map extended to include FAV_TYPES alongside SYNC_TYPES, with Series/Episode TVDB IDs namespaced to avoid collisions.
This commit is contained in:
@@ -691,6 +691,7 @@
|
||||
PLAY_SYNC_REMOTE=true # sync across hosts via Tailscale
|
||||
PLAY_SYNC_DAYS=90 # days back to check for played items (0 = all time)
|
||||
PLAY_SYNC_TYPES="Movie,Episode" # Audio excluded — music library too large; favorites handled separately
|
||||
PLAY_SYNC_FAV_TYPES="MusicArtist,MusicAlbum,Movie,Series" # union sync — never unmarks; Audio tracks future
|
||||
|
||||
# ━━━ Play State Sync — Handback ━━━
|
||||
# play_state_sync runs before DNS cutover so users land on current watch state.
|
||||
|
||||
+162
-9
@@ -94,9 +94,10 @@ parse_args "${_FILTERED[@]}"
|
||||
[[ "${PLAY_SYNC_ENABLED:-true}" != "true" ]] && echo "Play state sync disabled" && exit 0
|
||||
|
||||
SYNC_DAYS="${PLAY_SYNC_DAYS:-90}"
|
||||
SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode,Audio}"
|
||||
SYNC_TYPES="${PLAY_SYNC_TYPES:-Movie,Episode}"
|
||||
FAV_TYPES="${PLAY_SYNC_FAV_TYPES:-MusicArtist,MusicAlbum,Movie,Series}"
|
||||
[[ "$FULL_SYNC" == true ]] && SYNC_DAYS=0
|
||||
log "$ICON_GEAR Config: days=${SYNC_DAYS} types=${SYNC_TYPES} remote=${PLAY_SYNC_REMOTE:-true}"
|
||||
log "$ICON_GEAR Config: days=${SYNC_DAYS} types=${SYNC_TYPES} favs=${FAV_TYPES} remote=${PLAY_SYNC_REMOTE:-true}"
|
||||
|
||||
command -v jq >/dev/null 2>&1 || { error "jq is required but not installed"; exit 1; }
|
||||
|
||||
@@ -287,7 +288,15 @@ declare -A PROV_LOOKUP # "si|tvdb.{id}" | "si|imdb.{id}" | "si|mb.{id}" → it
|
||||
# pushing items to positions far beyond the first pages. Query each type separately so
|
||||
# each list sorts within its own type and items appear where expected.
|
||||
_PROV_PAGE=2000
|
||||
IFS=',' read -ra _prov_types <<< "$SYNC_TYPES"
|
||||
# Combine play state types + fav types for the map build (deduplicated)
|
||||
declare -A _prov_seen_dedup
|
||||
_prov_types=()
|
||||
IFS=',' read -ra _prov_combined <<< "${SYNC_TYPES},${FAV_TYPES}"
|
||||
for _t in "${_prov_combined[@]}"; do
|
||||
[[ -n "${_prov_seen_dedup[$_t]:-}" ]] && continue
|
||||
_prov_seen_dedup[$_t]=1; _prov_types+=("$_t")
|
||||
done
|
||||
unset _prov_seen_dedup _prov_combined
|
||||
|
||||
for _psi in $(seq 0 $(( _srv_count - 1 ))); do
|
||||
[[ -z "${SRV_URL[$_psi]}" ]] && continue
|
||||
@@ -303,17 +312,29 @@ for _psi in $(seq 0 $(( _srv_count - 1 ))); do
|
||||
[[ "$_prov_total" -lt 0 ]] && _prov_total=$(echo "$_page" | jq '.TotalRecordCount // 0' 2>/dev/null || echo 0)
|
||||
_prov_count=$(echo "$_page" | jq '.Items | length' 2>/dev/null || echo 0)
|
||||
[[ "$_prov_count" -eq 0 ]] && break
|
||||
while IFS=$'\t' read -r _pid _ptvdb _pimdb _ptmdb _pmbtrack; do
|
||||
[[ "$_ptvdb" != "null" && -n "$_ptvdb" ]] && PROV_LOOKUP["${_psi}|tvdb.${_ptvdb}"]="$_pid"
|
||||
[[ "$_pimdb" != "null" && -n "$_pimdb" ]] && PROV_LOOKUP["${_psi}|imdb.${_pimdb}"]="$_pid"
|
||||
[[ "$_ptmdb" != "null" && -n "$_ptmdb" ]] && PROV_LOOKUP["${_psi}|tmdb.${_ptmdb}"]="$_pid"
|
||||
[[ "$_pmbtrack" != "null" && -n "$_pmbtrack" ]] && PROV_LOOKUP["${_psi}|mb.${_pmbtrack}"]="$_pid"
|
||||
while IFS=$'\t' read -r _pid _ptype _ptvdb _pimdb _ptmdb _pmbtrack _pmbartist _pmbalbum; do
|
||||
[[ "$_pimdb" != "null" && -n "$_pimdb" ]] && PROV_LOOKUP["${_psi}|imdb.${_pimdb}"]="$_pid"
|
||||
[[ "$_ptmdb" != "null" && -n "$_ptmdb" ]] && PROV_LOOKUP["${_psi}|tmdb.${_ptmdb}"]="$_pid"
|
||||
[[ "$_pmbtrack" != "null" && -n "$_pmbtrack" ]] && PROV_LOOKUP["${_psi}|mb.${_pmbtrack}"]="$_pid"
|
||||
[[ "$_pmbartist" != "null" && -n "$_pmbartist" ]] && PROV_LOOKUP["${_psi}|mb.artist.${_pmbartist}"]="$_pid"
|
||||
[[ "$_pmbalbum" != "null" && -n "$_pmbalbum" ]] && PROV_LOOKUP["${_psi}|mb.album.${_pmbalbum}"]="$_pid"
|
||||
# tvdb: namespace by type so Series and Episode IDs don't collide
|
||||
if [[ "$_ptvdb" != "null" && -n "$_ptvdb" ]]; then
|
||||
case "$_ptype" in
|
||||
Series) PROV_LOOKUP["${_psi}|tvdb.series.${_ptvdb}"]="$_pid" ;;
|
||||
Episode) PROV_LOOKUP["${_psi}|tvdb.${_ptvdb}"]="$_pid" ;;
|
||||
*) PROV_LOOKUP["${_psi}|tvdb.${_ptvdb}"]="$_pid" ;;
|
||||
esac
|
||||
fi
|
||||
done < <(echo "$_page" | jq -r '.Items[] | [
|
||||
.Id,
|
||||
.Type,
|
||||
(.ProviderIds.Tvdb // "null"),
|
||||
(.ProviderIds.Imdb // "null"),
|
||||
(.ProviderIds.Tmdb // "null"),
|
||||
(.ProviderIds.MusicBrainzTrackId // "null")
|
||||
(.ProviderIds.MusicBrainzTrackId // "null"),
|
||||
(.ProviderIds.MusicBrainzArtistId // "null"),
|
||||
(.ProviderIds.MusicBrainzAlbumId // "null")
|
||||
] | @tsv' 2>/dev/null)
|
||||
_prov_start=$(( _prov_start + _prov_count ))
|
||||
[[ "$_prov_total" -gt 0 && "$_prov_start" -ge "$_prov_total" ]] && break
|
||||
@@ -584,6 +605,136 @@ for lname in "${!USER_MAP[@]}"; do
|
||||
unset ITEM_MAP U_IDX U_UID
|
||||
done
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Favorite Sync ━━━
|
||||
# ==============================================================================================
|
||||
# Union semantics — if favorited on any server, sync to all others. Never unmarks.
|
||||
# Covers MusicArtist, MusicAlbum, Movie, Series (configured via PLAY_SYNC_FAV_TYPES).
|
||||
FAV_TOTAL_SYNCED=0
|
||||
FAV_TOTAL_SKIPPED=0
|
||||
FAV_TOTAL_ERRORS=0
|
||||
|
||||
IFS=',' read -ra _fav_type_list <<< "$FAV_TYPES"
|
||||
|
||||
for lname in "${!USER_MAP[@]}"; do
|
||||
read -ra _pairs <<< "${USER_MAP[$lname]}"
|
||||
[[ "${#_pairs[@]}" -lt 2 ]] && continue
|
||||
|
||||
declare -A UF_IDX UF_UID
|
||||
for _pair in "${_pairs[@]}"; do
|
||||
IFS=':' read -r _si _ui <<< "$_pair"
|
||||
UF_IDX["$_si"]="$_si"
|
||||
UF_UID["$_si"]="$_ui"
|
||||
done
|
||||
|
||||
declare -A FAV_MAP # provider_key → space-separated "si:iid" pairs
|
||||
|
||||
for _si in "${!UF_IDX[@]}"; do
|
||||
_uid="${UF_UID[$_si]}"
|
||||
for _ftype in "${_fav_type_list[@]}"; do
|
||||
_resp=$(_api_get "${SRV_URL[$_si]}" "${SRV_KEY[$_si]}" \
|
||||
"Users/${_uid}/Items?Recursive=true&IncludeItemTypes=${_ftype}&Filters=IsFavorite&Fields=ProviderIds")
|
||||
[[ -z "$_resp" ]] && continue
|
||||
_fcount=$(echo "$_resp" | jq '.Items | length' 2>/dev/null || echo 0)
|
||||
[[ "$_fcount" -eq 0 ]] && continue
|
||||
log " ${SRV_NAME[$_si]} — $_fcount ${_ftype} favorite(s) for $lname"
|
||||
|
||||
while IFS=$'\t' read -r _iid _itype _pimdb _ptmdb _ptvdb _pmbartist _pmbalbum _pmbtrack; do
|
||||
_pkey=""
|
||||
case "$_itype" in
|
||||
Movie)
|
||||
[[ "$_pimdb" != "null" && -n "$_pimdb" ]] && _pkey="imdb:${_pimdb}"
|
||||
[[ -z "$_pkey" && "$_ptmdb" != "null" && -n "$_ptmdb" ]] && _pkey="tmdb:movie:${_ptmdb}"
|
||||
;;
|
||||
Series)
|
||||
[[ "$_ptvdb" != "null" && -n "$_ptvdb" ]] && _pkey="tvdb:series:${_ptvdb}"
|
||||
;;
|
||||
MusicArtist)
|
||||
[[ "$_pmbartist" != "null" && -n "$_pmbartist" ]] && _pkey="mb:artist:${_pmbartist}"
|
||||
;;
|
||||
MusicAlbum)
|
||||
[[ "$_pmbalbum" != "null" && -n "$_pmbalbum" ]] && _pkey="mb:album:${_pmbalbum}"
|
||||
;;
|
||||
Audio)
|
||||
[[ "$_pmbtrack" != "null" && -n "$_pmbtrack" ]] && _pkey="mb:track:${_pmbtrack}"
|
||||
;;
|
||||
esac
|
||||
[[ -z "$_pkey" ]] && continue
|
||||
|
||||
if [[ -n "${FAV_MAP[$_pkey]:-}" ]]; then
|
||||
FAV_MAP[$_pkey]+=" ${_si}:${_iid}"
|
||||
else
|
||||
FAV_MAP[$_pkey]="${_si}:${_iid}"
|
||||
fi
|
||||
done < <(echo "$_resp" | jq -r '.Items[] | [
|
||||
.Id, .Type,
|
||||
(.ProviderIds.Imdb // "null"),
|
||||
(.ProviderIds.Tmdb // "null"),
|
||||
(.ProviderIds.Tvdb // "null"),
|
||||
(.ProviderIds.MusicBrainzArtistId // "null"),
|
||||
(.ProviderIds.MusicBrainzAlbumId // "null"),
|
||||
(.ProviderIds.MusicBrainzTrackId // "null")
|
||||
] | @tsv' 2>/dev/null)
|
||||
done
|
||||
done
|
||||
|
||||
for _pkey in "${!FAV_MAP[@]}"; do
|
||||
declare -A _fhave _fiid
|
||||
for _entry in ${FAV_MAP[$_pkey]}; do
|
||||
IFS=':' read -r _si _iid <<< "$_entry"
|
||||
_fhave[$_si]=true
|
||||
_fiid[$_si]="$_iid"
|
||||
done
|
||||
|
||||
for _si in "${!UF_IDX[@]}"; do
|
||||
[[ "${_fhave[$_si]:-false}" == "true" ]] && (( FAV_TOTAL_SKIPPED++ )) && continue
|
||||
_uid="${UF_UID[$_si]}"
|
||||
|
||||
# Look up item ID on target server via PROV_LOOKUP
|
||||
_iid=""
|
||||
_ptype="${_pkey%%:*}"
|
||||
case "$_ptype" in
|
||||
imdb) _iid="${PROV_LOOKUP[${_si}|imdb.${_pkey#imdb:}]:-}" ;;
|
||||
tmdb) _iid="${PROV_LOOKUP[${_si}|tmdb.${_pkey#tmdb:movie:}]:-}" ;;
|
||||
tvdb) _iid="${PROV_LOOKUP[${_si}|tvdb.series.${_pkey#tvdb:series:}]:-}" ;;
|
||||
mb)
|
||||
_mbsub="${_pkey#mb:}"
|
||||
case "${_mbsub%%:*}" in
|
||||
artist) _iid="${PROV_LOOKUP[${_si}|mb.artist.${_mbsub#artist:}]:-}" ;;
|
||||
album) _iid="${PROV_LOOKUP[${_si}|mb.album.${_mbsub#album:}]:-}" ;;
|
||||
track) _iid="${PROV_LOOKUP[${_si}|mb.${_mbsub#track:}]:-}" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "$_iid" ]]; then
|
||||
log " SKIP FAV $_pkey → ${SRV_NAME[$_si]} not in library"
|
||||
continue
|
||||
fi
|
||||
|
||||
log " FAV $_pkey → ${SRV_NAME[$_si]} ($lname)"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
_http=$(_api_post "${SRV_URL[$_si]}" "${SRV_KEY[$_si]}" \
|
||||
"Users/${_uid}/FavoriteItems/${_iid}")
|
||||
if [[ "$_http" == "200" || "$_http" == "201" ]]; then
|
||||
success " ✓ FAV $_pkey → ${SRV_NAME[$_si]}"
|
||||
(( FAV_TOTAL_SYNCED++ ))
|
||||
else
|
||||
warn " ✗ FAV $_pkey → ${SRV_NAME[$_si]} failed (HTTP ${_http:-err})"
|
||||
(( FAV_TOTAL_ERRORS++ ))
|
||||
fi
|
||||
else
|
||||
echo " DRY RUN: would favorite $_pkey → ${SRV_NAME[$_si]} user=$lname"
|
||||
(( FAV_TOTAL_SYNCED++ ))
|
||||
fi
|
||||
done
|
||||
|
||||
unset _fhave _fiid
|
||||
done
|
||||
|
||||
unset FAV_MAP UF_IDX UF_UID
|
||||
done
|
||||
|
||||
# ==============================================================================================
|
||||
# ━━━ Summary ━━━
|
||||
# ==============================================================================================
|
||||
@@ -594,6 +745,8 @@ echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
|
||||
echo "$ICON_DONE Synced: $TOTAL_SYNCED"
|
||||
[[ "$TOTAL_SKIPPED" -gt 0 ]] && echo "$ICON_RUNNING Skipped: $TOTAL_SKIPPED (already current)"
|
||||
[[ "$TOTAL_ERRORS" -gt 0 ]] && echo "$ICON_ERROR Errors: $TOTAL_ERRORS"
|
||||
echo ""
|
||||
echo " Favorites — synced: $FAV_TOTAL_SYNCED skipped: $FAV_TOTAL_SKIPPED errors: $FAV_TOTAL_ERRORS"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — no changes written"
|
||||
|
||||
Reference in New Issue
Block a user