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:
Gmer4Lfe
2026-06-22 23:42:39 -04:00
parent 4de10a7d01
commit d058cf15c9
2 changed files with 163 additions and 9 deletions
+162 -9
View File
@@ -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"