Skip to content

Instantly share code, notes, and snippets.

@SingularReza
Last active August 27, 2025 05:04
Show Gist options
  • Select an option

  • Save SingularReza/c2063c017364564b165447147341ca12 to your computer and use it in GitHub Desktop.

Select an option

Save SingularReza/c2063c017364564b165447147341ca12 to your computer and use it in GitHub Desktop.
A bash script to import "LOVE RATING" values from FLAC metadata to jellyfin favorites and vice versa. Generated from pseudocode using AI so use it at your own risk
#!/bin/bash
###################################################################################
# Jellyfin FLAC Favorites and Play Count Bidirectional Sync Script
###################################################################################
#
# DESCRIPTION:
# This script provides bidirectional synchronization between FLAC files and
# Jellyfin favorites/play counts based on Vorbis comment tags.
#
# WHAT IT DOES:
# DIRECTION 1: FLAC → Jellyfin
# - Scans FLAC files with "LOVE RATING" = "L" and marks as favorites
# - Updates play counts from "PLAYCOUNT" vorbis comment tags
# - Can control whether to sync favorites, playcount, or both
# - Can limit to N most recently modified files
#
# DIRECTION 2: Jellyfin → FLAC
# - Gets favorite songs from Jellyfin and sets "LOVE RATING" = "L"
# - Can sort by "last played" or "last added" and limit results
#
# UNICODE FILENAME SUPPORT:
# Sets locale specifically for metaflac commands to handle UTF-8 metadata.
#
###################################################################################
#
# USAGE INSTRUCTIONS AND EXAMPLES
#
###################################################################################
#
# BASIC SYNTAX:
# ./script.sh <FOLDER> <API_KEY> [SYNC_DIRECTION] [SORT_CRITERIA] [ITEM_LIMIT] [FLAC_LIMIT] [PLAYCOUNT_LIMIT] [FLAC_SYNC_TYPE]
#
# PARAMETERS EXPLAINED:
# FOLDER - Full path to your music directory containing FLAC files
# Script searches recursively through all subdirectories
# Use quotes if path contains spaces
#
# API_KEY - Your Jellyfin API key (32+ character string)
# Get this from: Jellyfin Admin → Advanced → API Keys
#
# SYNC_DIRECTION - Optional, defaults to "both"
# "both" = Sync in both directions
# "to-jellyfin" = Only FLAC → Jellyfin (favorites + play counts)
# "to-flac" = Only Jellyfin → FLAC (favorites only)
#
# SORT_CRITERIA - Optional, for Jellyfin → FLAC sync only
# "last-played" = Sort favorites by last played date
# "last-added" = Sort favorites by date added to library
# Leave empty ("") if not needed
#
# ITEM_LIMIT - Optional, requires SORT_CRITERIA
# Number of most recent Jellyfin favorites to process
# Only affects Jellyfin → FLAC direction
#
# FLAC_LIMIT - Optional, for FLAC → Jellyfin favorites sync
# Number of most recently modified FLAC files to process
# Only affects favorites sync, not play count sync
#
# PLAYCOUNT_LIMIT - Optional, for FLAC → Jellyfin play count sync
# Number of most recently modified FLAC files to process
# Independent from FLAC_LIMIT
#
# FLAC_SYNC_TYPE - Optional, defaults to "both", controls FLAC → Jellyfin sync
# "both" = Sync both favorites and play counts
# "favorites" = Sync only favorites (ignore PLAYCOUNT tags)
# "playcount" = Sync only play counts (ignore LOVE RATING tags)
# Only affects "to-jellyfin" and "both" sync directions
#
###################################################################################
#
# USAGE EXAMPLES:
#
# 1. BASIC USAGE - Full bidirectional sync (all files, both favorites and play counts):
# ./jellyfin_sync.sh "/home/user/Music" "your-jellyfin-api-key"
#
# This will:
# - Process ALL FLAC files for favorites and play counts → Jellyfin
# - Process ALL Jellyfin favorites → FLAC files
#
# 2. FAVORITES ONLY - Sync only favorites, ignore play counts completely:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "both" "" "" "" "" "favorites"
#
# This will:
# - Sync favorites in both directions
# - Skip play count sync completely
# - Faster than full sync
#
# 3. PLAY COUNTS ONLY - Sync only play counts, ignore favorites:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "to-jellyfin" "" "" "" 100 "playcount"
#
# This will:
# - Read FLAC "PLAYCOUNT" tags → update Jellyfin play counts (100 recent files)
# - Ignore FLAC "LOVE RATING" tags completely
# - NOT modify any FLAC files or sync favorites
#
# 4. ONE-WAY SYNC - FLAC to Jellyfin only (both types):
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "to-jellyfin"
#
# This will:
# - Read FLAC "LOVE RATING" tags → mark Jellyfin favorites
# - Read FLAC "PLAYCOUNT" tags → update Jellyfin play counts
# - NOT modify any FLAC files
#
# 5. ONE-WAY SYNC - Jellyfin to FLAC only:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "to-flac"
#
# This will:
# - Read Jellyfin favorites → set FLAC "LOVE RATING" = "L"
# - NOT read FLAC files or update Jellyfin
# - FLAC_SYNC_TYPE parameter is ignored for this direction
#
# 6. LIMITED RECENT FILES - Process only recently modified files with type control:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "to-jellyfin" "" "" 50 100 "both"
#
# This will:
# - Process 50 most recently modified FLAC files for favorites
# - Process 100 most recently modified FLAC files for play counts
# - Useful for regular incremental updates
#
# 7. FAVORITES ONLY WITH LIMITS - Fast favorites-only sync:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "both" "last-played" 25 50 "" "favorites"
#
# This will:
# - FLAC → Jellyfin: 50 recent files for favorites only
# - Jellyfin → FLAC: 25 most recently played favorites
# - Skip all play count processing (faster)
#
# 8. PLAY COUNTS MAINTENANCE - Update only play counts from recent files:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "to-jellyfin" "" "" "" 200 "playcount"
#
# This will:
# - Process 200 most recently modified FLAC files for play counts only
# - Skip favorites processing entirely
# - Good for daily play count updates
#
# 9. TESTING - Process only a few files for testing:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "to-jellyfin" "" "" 5 10 "both"
#
# This will:
# - Process only 5 recent FLAC files for favorites
# - Process only 10 recent FLAC files for play counts
# - Good for testing the script before full sync
#
# 10. DAILY SYNC - Typical daily maintenance (favorites only for speed):
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "both" "last-played" 100 50 "" "favorites"
#
# This will:
# - Sync 50 most recently modified FLAC files (favorites only)
# - Sync 100 most recently played Jellyfin favorites
# - Good for daily cron job (faster than full sync)
#
# 11. WEEKLY PLAY COUNT UPDATE - Update play counts from all recent activity:
# ./jellyfin_sync.sh "/home/user/Music" "your-api-key" "to-jellyfin" "" "" "" 500 "playcount"
#
# This will:
# - Update play counts from 500 most recently modified FLAC files
# - Skip favorites (if you sync those daily)
# - Good for weekly cron job
#
# 12. MUSIC PATH WITH SPACES - Handle paths containing spaces:
# ./jellyfin_sync.sh "/home/user/My Music Collection" "your-api-key" "both" "" "" "" "" "favorites"
#
# Always use quotes around paths with spaces
#
###################################################################################
#
# COMMON USE CASES:
#
# INITIAL SETUP - First time running the script:
# 1. Backup your FLAC files first!
# 2. Start with a test: ./script.sh "/path/to/music" "api-key" "to-jellyfin" "" "" 10 10 "both"
# 3. Check results, then run full sync: ./script.sh "/path/to/music" "api-key" "both"
#
# REGULAR MAINTENANCE - Daily/weekly updates:
# Daily favorites: ./script.sh "/path/to/music" "api-key" "both" "last-played" 100 50 "" "favorites"
# Weekly play counts: ./script.sh "/path/to/music" "api-key" "to-jellyfin" "" "" "" 500 "playcount"
#
# PERFORMANCE OPTIMIZATION - For large libraries:
# Fast daily sync: Use "favorites" only with small limits (50-100 files)
# Weekly full sync: Use "both" with larger limits or no limits
# Play count only: Use "playcount" when you only need statistics updates
#
# AFTER IMPORTING NEW MUSIC - Sync only recent files:
# ./script.sh "/path/to/music" "api-key" "to-jellyfin" "" "" 100 100 "both"
#
# BACKUP JELLYFIN FAVORITES - Save favorites to FLAC metadata:
# ./script.sh "/path/to/music" "api-key" "to-flac" "last-added" 1000
#
# MIGRATE FROM OTHER PLAYER - Import existing ratings and play counts:
# ./script.sh "/path/to/music" "api-key" "to-jellyfin" "" "" "" "" "both"
#
# SELECTIVE SYNC - Choose what to sync based on your needs:
# Only care about favorites: Use "favorites" type
# Only care about statistics: Use "playcount" type
# Want everything: Use "both" type (default)
#
###################################################################################
#
# IMPORTANT NOTES:
#
# 1. BACKUP FIRST! This script modifies FLAC file metadata
# cp -r "/your/music/folder" "/backup/location/"
#
# 2. FLAC METADATA REQUIREMENTS:
# - "LOVE RATING" tag must be exactly "L" (case sensitive)
# - "PLAYCOUNT" tag must be a positive integer (1, 2, 3, etc.)
# - Files must have "TITLE" and "ALBUM" tags for matching
#
# 3. JELLYFIN SETUP:
# - Get API key from: Admin Dashboard → Advanced → API Keys
# - Ensure your music library is fully scanned
# - Update JELLYFIN_URL variable in script if not localhost:8096
#
# 4. PERFORMANCE TIPS:
# - Use "favorites" type for faster daily syncs
# - Use "playcount" type for statistics-only updates
# - Use limits for large libraries (10,000+ files)
# - Play count sync is slower (multiple API calls per file)
# - Run with smaller limits first to test
#
# 5. SYNC TYPE USAGE:
# - "favorites": Fast, only processes LOVE RATING tags
# - "playcount": Slower, only processes PLAYCOUNT tags
# - "both": Complete sync, processes both tag types
# - FLAC_SYNC_TYPE only affects FLAC → Jellyfin direction
#
# 6. LIMITATIONS:
# - Cannot decrease play counts in Jellyfin (API limitation)
# - Exact TITLE/ALBUM matching required
# - Unicode filenames require proper locale support
# - FLAC_SYNC_TYPE is ignored for "to-flac" sync direction
#
# 7. TROUBLESHOOTING:
# - Check /tmp/jellyfin_favorites_debug.json for API responses
# - Run with small limits first (5-10 files) to debug issues
# - Ensure FLAC files are writable for metadata updates
# - Use "favorites" type first to test basic functionality
#
###################################################################################
#
# CRON JOB EXAMPLES:
#
# Fast daily favorites sync at 2 AM:
# 0 2 * * * /path/to/jellyfin_sync.sh "/home/user/Music" "api-key" "both" "last-played" 100 50 "" "favorites" >> /var/log/jellyfin_sync_daily.log 2>&1
#
# Weekly full sync on Sunday at 3 AM:
# 0 3 * * 0 /path/to/jellyfin_sync.sh "/home/user/Music" "api-key" "both" "last-played" 500 200 300 "both" >> /var/log/jellyfin_sync_weekly.log 2>&1
#
# Play count only sync every 6 hours:
# 0 */6 * * * /path/to/jellyfin_sync.sh "/home/user/Music" "api-key" "to-jellyfin" "" "" "" 100 "playcount" >> /var/log/jellyfin_sync_playcount.log 2>&1
#
# Hourly favorites only (very recent files):
# 0 * * * * /path/to/jellyfin_sync.sh "/home/user/Music" "api-key" "both" "" "" 10 "" "favorites" >> /var/log/jellyfin_sync_hourly.log 2>&1
#
###################################################################################
# Check if required arguments are provided
if [ $# -lt 2 ] || [ $# -gt 8 ]; then
echo "Usage: $0 <FOLDER> <API_KEY> [SYNC_DIRECTION] [SORT_CRITERIA] [ITEM_LIMIT] [FLAC_LIMIT] [PLAYCOUNT_LIMIT] [FLAC_SYNC_TYPE]"
echo " FOLDER: Directory path containing FLAC files"
echo " API_KEY: Jellyfin API key"
echo " SYNC_DIRECTION: 'both' (default), 'to-jellyfin', or 'to-flac'"
echo " SORT_CRITERIA: 'last-played' or 'last-added' (for to-flac/both modes)"
echo " ITEM_LIMIT: Number of most recent items to process (requires SORT_CRITERIA)"
echo " FLAC_LIMIT: Number of most recently modified FLAC files for favorites sync"
echo " PLAYCOUNT_LIMIT: Number of most recently modified FLAC files for play count sync"
echo " FLAC_SYNC_TYPE: 'both' (default), 'favorites', or 'playcount' (for FLAC→Jellyfin sync)"
echo ""
echo "Examples:"
echo " $0 \"/home/user/Music\" \"your-api-key\""
echo " $0 \"/mnt/music\" \"your-api-key\" \"both\" \"\" \"\" \"\" \"\" \"favorites\""
echo " $0 \"/home/user/Music\" \"your-api-key\" \"to-jellyfin\" \"\" \"\" 100 50 \"both\""
echo " $0 \"/home/user/Music\" \"your-api-key\" \"both\" \"last-played\" 25 50 100 \"playcount\""
echo ""
echo "See script comments for detailed usage instructions and examples"
exit 1
fi
FOLDER="$1"
KEY="$2"
SYNC_DIRECTION="${3:-both}"
SORT_CRITERIA="$4"
ITEM_LIMIT="$5"
FLAC_LIMIT="$6"
PLAYCOUNT_LIMIT="$7"
FLAC_SYNC_TYPE="${8:-both}"
# Validate sync direction
if [[ ! "$SYNC_DIRECTION" =~ ^(both|to-jellyfin|to-flac)$ ]]; then
echo "Error: Invalid sync direction. Use 'both', 'to-jellyfin', or 'to-flac'"
exit 1
fi
# Validate sort criteria if provided
if [ -n "$SORT_CRITERIA" ]; then
if [[ ! "$SORT_CRITERIA" =~ ^(last-played|last-added)$ ]]; then
echo "Error: Invalid sort criteria. Use 'last-played' or 'last-added'"
exit 1
fi
fi
# Validate item limit if provided
if [ -n "$ITEM_LIMIT" ]; then
if [ -z "$SORT_CRITERIA" ]; then
echo "Error: ITEM_LIMIT requires SORT_CRITERIA to be specified"
exit 1
fi
if ! [[ "$ITEM_LIMIT" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: ITEM_LIMIT must be a positive integer"
exit 1
fi
fi
# Validate FLAC limit if provided
if [ -n "$FLAC_LIMIT" ]; then
if ! [[ "$FLAC_LIMIT" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: FLAC_LIMIT must be a positive integer"
exit 1
fi
fi
# Validate play count limit if provided
if [ -n "$PLAYCOUNT_LIMIT" ]; then
if ! [[ "$PLAYCOUNT_LIMIT" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: PLAYCOUNT_LIMIT must be a positive integer"
exit 1
fi
fi
# Validate FLAC sync type if provided
if [[ ! "$FLAC_SYNC_TYPE" =~ ^(both|favorites|playcount)$ ]]; then
echo "Error: Invalid FLAC sync type. Use 'both', 'favorites', or 'playcount'"
exit 1
fi
# Check if folder exists
if [ ! -d "$FOLDER" ]; then
echo "Error: Directory '$FOLDER' does not exist"
echo "Please check the path and try again."
exit 1
fi
# Check if required tools are available
if ! command -v metaflac &> /dev/null; then
echo "Error: metaflac command not found."
echo "Install with: sudo apt install flac (Ubuntu/Debian) or brew install flac (macOS)"
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "Error: jq command not found."
echo "Install with: sudo apt install jq (Ubuntu/Debian) or brew install jq (macOS)"
exit 1
fi
if ! command -v curl &> /dev/null; then
echo "Error: curl command not found."
echo "Install with: sudo apt install curl (Ubuntu/Debian) or brew install curl (macOS)"
exit 1
fi
# Jellyfin server URL - MODIFY THIS if your server is not on localhost:8096
JELLYFIN_URL="http://localhost:8096"
# Function to run metaflac with proper UTF-8 locale
run_metaflac() {
# Try different locale options for UTF-8 support
local locale_options=("en_US.UTF-8" "C.UTF-8" "en_GB.UTF-8")
for loc in "${locale_options[@]}"; do
if locale -a 2>/dev/null | grep -q "^${loc}$"; then
LC_ALL="$loc" LANG="$loc" metaflac "$@"
return $?
fi
done
# Fallback to system default
metaflac "$@"
return $?
}
# Function to get user ID (needed for favorites API)
get_user_id() {
curl -s -H "X-Emby-Token: $KEY" \
"$JELLYFIN_URL/Users" | \
jq -r '.[0].Id' 2>/dev/null
}
# Enhanced function to get all favorite items with additional debugging
get_favorites_with_paths() {
local user_id="$1"
local sort_criteria="$2"
local item_limit="$3"
# Build URL with base parameters
local url="$JELLYFIN_URL/Users/$user_id/Items?isFavorite=true&includeItemTypes=Audio&recursive=true&fields=Path,MediaSources"
# Add sorting parameters if specified
if [ -n "$sort_criteria" ]; then
case "$sort_criteria" in
"last-played")
url="${url}&sortBy=DatePlayed&sortOrder=Descending"
;;
"last-added")
url="${url}&sortBy=DateCreated&sortOrder=Descending"
;;
esac
fi
# Add limit if specified
if [ -n "$item_limit" ]; then
url="${url}&limit=$item_limit"
fi
echo "[DEBUG] API URL: $url" >&2
# Get raw response and save for debugging
local raw_response
raw_response=$(curl -s -H "X-Emby-Token: $KEY" "$url")
# Save raw response to file for debugging
echo "$raw_response" > "/tmp/jellyfin_favorites_debug.json"
echo "[DEBUG] Raw API response saved to /tmp/jellyfin_favorites_debug.json" >&2
# Try different approaches to get file paths
echo "$raw_response" | jq -r '.Items[] |
if .Path then
"\(.Name)|\(.Album)|\(.Path)"
elif (.MediaSources | length > 0) then
"\(.Name)|\(.Album)|\(.MediaSources[0].Path)"
else
"\(.Name)|\(.Album)|NO_PATH_AVAILABLE"
end'
}
# Function to get FLAC files sorted by modification time
get_recent_flac_files() {
local folder="$1"
local limit="$2"
echo "Getting FLAC files sorted by modification time..." >&2
if [ -n "$limit" ]; then
echo "Limiting to $limit most recently modified files" >&2
# Use find with printf to get modification time and path, sort by time (newest first), take top N
find "$folder" -type f -iname "*.flac" -printf '%T@ %p\n' 2>/dev/null | \
sort -nr | \
head -n "$limit" | \
cut -d' ' -f2-
else
echo "Processing all FLAC files" >&2
find "$folder" -type f -iname "*.flac" 2>/dev/null
fi
}
# Function to search for item in Jellyfin
search_item() {
local album="$1"
local title="$2"
local user_id="$3"
# URL encode the search terms
local encoded_title=$(printf '%s' "$title" | jq -sRr @uri)
# Search for the item by title first, then filter by album
curl -s -H "X-Emby-Token: $KEY" \
"$JELLYFIN_URL/Users/$user_id/Items?searchTerm=$encoded_title&includeItemTypes=Audio&recursive=true" | \
jq -r --arg album "$album" --arg title "$title" \
'.Items[] | select(.Album == $album and .Name == $title) | .Id' | head -n 1
}
# Function to find FLAC file by filename and verify album (FIXED exclusion)
find_flac_by_filename() {
local filename="$1"
local expected_album="$2"
MATCH_ERROR=""
local excluded_files=()
local max_attempts=5
local attempt=1
echo " Debug: Searching for filename: '$filename'" >&2
echo " Debug: Expected album: '$expected_album'" >&2
while [ $attempt -le $max_attempts ]; do
echo " Debug: Attempt $attempt" >&2
if [ ${#excluded_files[@]} -gt 0 ]; then
echo " Debug: Excluding ${#excluded_files[@]} files: ${excluded_files[*]}" >&2
fi
local found_file=""
local search_method=""
local all_candidates=()
# First try: exact filename match (case-sensitive)
if [ ${#all_candidates[@]} -eq 0 ]; then
mapfile -t all_candidates < <(find "$FOLDER" -type f -name "$filename" 2>/dev/null)
search_method="exact match"
fi
# Second try: case-insensitive search if exact match fails
if [ ${#all_candidates[@]} -eq 0 ]; then
echo " Debug: Exact match failed, trying case-insensitive search..." >&2
mapfile -t all_candidates < <(find "$FOLDER" -type f -iname "$filename" 2>/dev/null)
search_method="case-insensitive match"
fi
# Third try: wildcard search in case there are encoding issues
if [ ${#all_candidates[@]} -eq 0 ]; then
echo " Debug: Case-insensitive failed, trying wildcard search..." >&2
local basename_without_ext="${filename%.*}"
mapfile -t all_candidates < <(find "$FOLDER" -type f -name "*${basename_without_ext}*" -iname "*.flac" 2>/dev/null)
search_method="wildcard search"
fi
# Filter out excluded files
for candidate in "${all_candidates[@]}"; do
local is_excluded=false
# Check if this candidate is in our exclusion list
for excluded in "${excluded_files[@]}"; do
if [ "$candidate" = "$excluded" ]; then
is_excluded=true
break
fi
done
# If not excluded, use this candidate
if [ "$is_excluded" = false ]; then
found_file="$candidate"
break
fi
done
# If no file found at all, exit with error
if [ -z "$found_file" ]; then
if [ ${#excluded_files[@]} -eq 0 ]; then
MATCH_ERROR="Filename '$filename' not found in directory (tried exact, case-insensitive, and wildcard searches)"
else
MATCH_ERROR="Filename '$filename' not found in directory after excluding ${#excluded_files[@]} files with wrong album tags"
fi
return 1
fi
echo " Debug: Found file via $search_method: '$found_file'" >&2
# Check if it's a FLAC file
if [[ ! "$found_file" =~ \.(flac|FLAC)$ ]]; then
echo " Debug: File is not FLAC, excluding and retrying..." >&2
excluded_files+=("$found_file")
((attempt++))
continue
fi
# Get ALBUM tag from the file using UTF-8 locale
local file_album
file_album=$(run_metaflac --show-tag=ALBUM "$found_file" 2>/dev/null | cut -d'=' -f2-)
if [ -z "$file_album" ]; then
echo " Debug: File has no ALBUM tag, excluding and retrying..." >&2
excluded_files+=("$found_file")
((attempt++))
continue
fi
echo " Debug: File album tag: '$file_album'" >&2
# Check if album matches
if [ "$file_album" != "$expected_album" ]; then
echo " Debug: ALBUM tag mismatch (expected: '$expected_album', found: '$file_album'), excluding and retrying..." >&2
excluded_files+=("$found_file")
((attempt++))
continue
fi
# Success - both filename found and album matches
echo " Debug: Album matches! Returning file: '$found_file'" >&2
echo "$found_file"
return 0
done
# If we get here, we've exhausted all attempts
MATCH_ERROR="Filename '$filename' found ${#excluded_files[@]} candidate files but none had matching album tag '$expected_album' after $max_attempts attempts"
return 1
}
# Function to mark item as favorite
mark_favorite() {
local item_id="$1"
local user_id="$2"
local response=$(curl -s -w "%{http_code}" -X POST -H "X-Emby-Token: $KEY" \
"$JELLYFIN_URL/Users/$user_id/FavoriteItems/$item_id")
local http_code="${response: -3}"
if [ "$http_code" = "200" ]; then
return 0
else
return 1
fi
}
# Function to update play count in Jellyfin
update_play_count() {
local item_id="$1"
local user_id="$2"
local play_count="$3"
echo " [DEBUG] Updating play count for item $item_id to $play_count" >&2
# First, get current play count to avoid unnecessary updates
local current_stats
current_stats=$(curl -s -H "X-Emby-Token: $KEY" \
"$JELLYFIN_URL/Users/$user_id/Items/$item_id")
local current_play_count
current_play_count=$(echo "$current_stats" | jq -r '.UserData.PlayCount // 0')
echo " [DEBUG] Current play count in Jellyfin: $current_play_count" >&2
# Only update if the play count is different
if [ "$current_play_count" != "$play_count" ]; then
# Jellyfin doesn't have a direct "set play count" API, so we need to simulate plays
# We'll use the reporting API to mark items as played
if [ "$play_count" -gt "$current_play_count" ]; then
local plays_to_add=$((play_count - current_play_count))
echo " [DEBUG] Adding $plays_to_add plays to reach target count" >&2
# Add the required number of plays
for ((i=1; i<=plays_to_add; i++)); do
local response
response=$(curl -s -w "%{http_code}" -X POST -H "X-Emby-Token: $KEY" \
"$JELLYFIN_URL/Users/$user_id/PlayedItems/$item_id")
local http_code="${response: -3}"
if [ "$http_code" != "200" ]; then
echo " [DEBUG] Failed to add play #$i (HTTP $http_code)" >&2
return 1
fi
done
return 0
elif [ "$play_count" -lt "$current_play_count" ]; then
echo " [DEBUG] Cannot decrease play count (FLAC: $play_count < Jellyfin: $current_play_count)" >&2
echo " [DEBUG] Jellyfin API doesn't support decreasing play counts" >&2
return 2 # Special return code for "cannot decrease"
else
echo " [DEBUG] Play counts already match" >&2
return 0
fi
else
echo " [DEBUG] Play counts already match ($play_count)" >&2
return 0
fi
}
# Function to set LOVE RATING tag in FLAC file
set_love_rating() {
local flac_file="$1"
local rating="$2"
local temp_error_file=$(mktemp)
echo " [DEBUG] Processing file: '$flac_file'" >&2
# Check basic file access
if [ ! -f "$flac_file" ]; then
echo " ✗ File does not exist: '$flac_file'" >&2
rm "$temp_error_file"
return 1
fi
# Check if metaflac can read the file
if ! run_metaflac --show-md5sum "$flac_file" >/dev/null 2>&1; then
echo " ✗ metaflac cannot read the file (may be corrupt or wrong format)" >&2
rm "$temp_error_file"
return 1
fi
# Check basic file permissions
if [ ! -w "$flac_file" ]; then
echo " ✗ File is not writable" >&2
echo " Try: chmod +w \"$flac_file\"" >&2
rm "$temp_error_file"
return 1
fi
echo " Attempting to remove existing LOVE RATING tag..." >&2
# Remove existing LOVE RATING tag (don't fail if tag doesn't exist)
run_metaflac --remove-tag="LOVE RATING" "$flac_file" 2>"$temp_error_file"
echo " Attempting to set new LOVE RATING tag..." >&2
# Add new LOVE RATING tag
if ! run_metaflac --set-tag="LOVE RATING=$rating" "$flac_file" 2>"$temp_error_file"; then
echo " ✗ Failed to set new tag:" >&2
cat "$temp_error_file" >&2
rm "$temp_error_file"
return 1
fi
# Verify the tag was actually set
echo " Verifying tag was set correctly..." >&2
local verify_rating
verify_rating=$(run_metaflac --show-tag="LOVE RATING" "$flac_file" 2>/dev/null | cut -d'=' -f2-)
if [ "$verify_rating" != "$rating" ]; then
echo " ✗ Tag verification failed. Expected: '$rating', Found: '$verify_rating'" >&2
rm "$temp_error_file"
return 1
fi
echo " ✓ Tag set and verified successfully" >&2
rm "$temp_error_file"
return 0
}
# Validation and connection test
echo "============================================"
echo "Jellyfin FLAC Sync Script with Play Counts"
echo "============================================"
echo "Folder: $FOLDER"
echo "Sync Direction: $SYNC_DIRECTION"
if [ -n "$SORT_CRITERIA" ]; then
echo "Sort Criteria: $SORT_CRITERIA"
fi
if [ -n "$ITEM_LIMIT" ]; then
echo "Item Limit: $ITEM_LIMIT"
fi
if [ -n "$FLAC_LIMIT" ]; then
echo "FLAC Limit (favorites): $FLAC_LIMIT (most recently modified files)"
fi
if [ -n "$PLAYCOUNT_LIMIT" ]; then
echo "FLAC Limit (play counts): $PLAYCOUNT_LIMIT (most recently modified files)"
fi
echo "FLAC Sync Type: $FLAC_SYNC_TYPE"
echo "Jellyfin URL: $JELLYFIN_URL"
echo ""
# Get user ID and test connection
echo "Connecting to Jellyfin server..."
USER_ID=$(get_user_id)
if [ -z "$USER_ID" ] || [ "$USER_ID" = "null" ]; then
echo "Error: Could not get user ID."
echo "Please check:"
echo " - Your API key is correct"
echo " - Jellyfin server is running at $JELLYFIN_URL"
echo " - Network connectivity to Jellyfin server"
exit 1
fi
echo "Successfully connected to Jellyfin"
echo "Using User ID: $USER_ID"
echo ""
# Count FLAC files for progress indication
flac_count=$(find "$FOLDER" -type f -iname "*.flac" | wc -l)
echo "Found $flac_count FLAC files in directory"
echo ""
# Counters
flac_processed=0
flac_to_jellyfin=0
playcount_processed=0
playcount_updated=0
jellyfin_processed=0
jellyfin_to_flac=0
# DIRECTION 1: FLAC → Jellyfin (Modified to support FLAC_SYNC_TYPE)
if [[ "$SYNC_DIRECTION" == "both" || "$SYNC_DIRECTION" == "to-jellyfin" ]]; then
echo "=========================================="
echo "PHASE 1: Syncing FLAC → Jellyfin"
echo "=========================================="
echo "Sync type: $FLAC_SYNC_TYPE"
echo ""
# PART 1A: Favorites sync (only if FLAC_SYNC_TYPE is "both" or "favorites")
if [[ "$FLAC_SYNC_TYPE" == "both" || "$FLAC_SYNC_TYPE" == "favorites" ]]; then
echo "--- Part 1A: Favorites Sync ---"
# Create temporary file for FLAC files (sorted by modification time if limit specified)
temp_file=$(mktemp)
get_recent_flac_files "$FOLDER" "$FLAC_LIMIT" > "$temp_file"
# FIXED: Remove 'local' keyword - this is not inside a function
files_to_process=$(wc -l < "$temp_file")
echo "Processing $files_to_process FLAC files for favorites$([ -n "$FLAC_LIMIT" ] && echo " (limited to $FLAC_LIMIT most recent)")"
echo ""
# Process each FLAC file for favorites
while IFS= read -r flac_file; do
echo "Processing: $(basename "$flac_file")"
echo " Last modified: $(date -r "$flac_file" '+%Y-%m-%d %H:%M:%S')"
flac_processed=$((flac_processed + 1))
# Read LOVE RATING tag using UTF-8 locale
love_rating=$(run_metaflac --show-tag="LOVE RATING" "$flac_file" 2>/dev/null | cut -d'=' -f2-)
# Check if LOVE RATING is "L"
if [ "$love_rating" = "L" ]; then
echo " ✓ Found LOVE RATING = L"
# Get ALBUM and TITLE tags using UTF-8 locale
album=$(run_metaflac --show-tag=ALBUM "$flac_file" 2>/dev/null | cut -d'=' -f2-)
title=$(run_metaflac --show-tag=TITLE "$flac_file" 2>/dev/null | cut -d'=' -f2-)
if [ -n "$album" ] && [ -n "$title" ]; then
echo " Album: $album"
echo " Title: $title"
echo " Searching in Jellyfin..."
# Search for item in Jellyfin
item_id=$(search_item "$album" "$title" "$USER_ID")
if [ -n "$item_id" ] && [ "$item_id" != "null" ] && [ "$item_id" != "" ]; then
echo " Found item ID: $item_id"
echo " Marking as favorite..."
# Mark as favorite
if mark_favorite "$item_id" "$USER_ID"; then
echo " ✓ Successfully marked as favorite"
flac_to_jellyfin=$((flac_to_jellyfin + 1))
else
echo " ✗ Failed to mark as favorite"
fi
else
echo " ✗ Item not found in Jellyfin library"
fi
else
echo " ✗ Missing ALBUM or TITLE tag in file metadata"
fi
else
echo " LOVE RATING is not 'L' (found: '$love_rating')"
fi
echo ""
done < "$temp_file"
# Clean up temporary file
rm "$temp_file"
else
echo "--- Part 1A: Favorites Sync (SKIPPED) ---"
echo "FLAC_SYNC_TYPE is '$FLAC_SYNC_TYPE', skipping favorites sync"
echo ""
fi
# PART 1B: Play count sync (only if FLAC_SYNC_TYPE is "both" or "playcount")
if [[ "$FLAC_SYNC_TYPE" == "both" || "$FLAC_SYNC_TYPE" == "playcount" ]]; then
echo ""
echo "--- Part 1B: Play Count Sync ---"
# Create temporary file for play count sync (different limit)
temp_file=$(mktemp)
get_recent_flac_files "$FOLDER" "$PLAYCOUNT_LIMIT" > "$temp_file"
# FIXED: Remove 'local' keyword - this is not inside a function
playcount_files_to_process=$(wc -l < "$temp_file")
echo "Processing $playcount_files_to_process FLAC files for play counts$([ -n "$PLAYCOUNT_LIMIT" ] && echo " (limited to $PLAYCOUNT_LIMIT most recent)")"
echo ""
# Process each FLAC file for play counts
while IFS= read -r flac_file; do
echo "Processing: $(basename "$flac_file")"
echo " Last modified: $(date -r "$flac_file" '+%Y-%m-%d %H:%M:%S')"
playcount_processed=$((playcount_processed + 1))
# Read PLAYCOUNT tag using UTF-8 locale
play_count=$(run_metaflac --show-tag="PLAYCOUNT" "$flac_file" 2>/dev/null | cut -d'=' -f2-)
# Check if PLAYCOUNT exists and is a valid number
if [ -n "$play_count" ] && [[ "$play_count" =~ ^[0-9]+$ ]] && [ "$play_count" -gt 0 ]; then
echo " ✓ Found PLAYCOUNT = $play_count"
# Get ALBUM and TITLE tags using UTF-8 locale
album=$(run_metaflac --show-tag=ALBUM "$flac_file" 2>/dev/null | cut -d'=' -f2-)
title=$(run_metaflac --show-tag=TITLE "$flac_file" 2>/dev/null | cut -d'=' -f2-)
if [ -n "$album" ] && [ -n "$title" ]; then
echo " Album: $album"
echo " Title: $title"
echo " Searching in Jellyfin..."
# Search for item in Jellyfin
item_id=$(search_item "$album" "$title" "$USER_ID")
if [ -n "$item_id" ] && [ "$item_id" != "null" ] && [ "$item_id" != "" ]; then
echo " Found item ID: $item_id"
echo " Updating play count to $play_count..."
# Update play count
local update_result
update_play_count "$item_id" "$USER_ID" "$play_count"
update_result=$?
case $update_result in
0)
echo " ✓ Successfully updated play count"
playcount_updated=$((playcount_updated + 1))
;;
1)
echo " ✗ Failed to update play count"
;;
2)
echo " ! Cannot decrease play count (FLAC < Jellyfin)"
;;
esac
else
echo " ✗ Item not found in Jellyfin library"
fi
else
echo " ✗ Missing ALBUM or TITLE tag in file metadata"
fi
else
if [ -n "$play_count" ]; then
echo " PLAYCOUNT is invalid or zero (found: '$play_count')"
else
echo " No PLAYCOUNT tag found"
fi
fi
echo ""
done < "$temp_file"
# Clean up temporary file
rm "$temp_file"
else
echo ""
echo "--- Part 1B: Play Count Sync (SKIPPED) ---"
echo "FLAC_SYNC_TYPE is '$FLAC_SYNC_TYPE', skipping play count sync"
echo ""
fi
fi
# DIRECTION 2: Jellyfin → FLAC (No changes from previous version)
if [[ "$SYNC_DIRECTION" == "both" || "$SYNC_DIRECTION" == "to-flac" ]]; then
echo "=========================================="
echo "PHASE 2: Syncing Jellyfin → FLAC"
echo "=========================================="
echo ""
echo "Getting favorites from Jellyfin..."
if [ -n "$SORT_CRITERIA" ]; then
echo "Sort criteria: $SORT_CRITERIA"
fi
if [ -n "$ITEM_LIMIT" ]; then
echo "Item limit: $ITEM_LIMIT"
fi
echo ""
# Get favorites with paths and process each line correctly
get_favorites_with_paths "$USER_ID" "$SORT_CRITERIA" "$ITEM_LIMIT" | while IFS= read -r line; do
# Split the line properly to handle trailing empty fields
IFS='|' read -r title album path <<< "$line"
# Trim whitespace from all variables
title=$(echo "$title" | xargs)
album=$(echo "$album" | xargs)
path=$(echo "$path" | xargs)
echo " [DEBUG] Raw line: '$line'" >&2
echo " [DEBUG] Parsed - Title: '$title' | Album: '$album' | Path: '$path'" >&2
if [ -n "$title" ] && [ -n "$album" ]; then
echo "Processing favorite: $title - $album"
jellyfin_processed=$((jellyfin_processed + 1))
if [ -n "$path" ] && [ "$path" != "NO_PATH_AVAILABLE" ]; then
# Use Path from Jellyfin API with retry approach
filename=$(basename "$path")
echo " Using Jellyfin path: $path"
echo " Filename: $filename"
echo " Expected album: $album"
echo " Searching for file in directory..."
matched_flac_file=$(find_flac_by_filename "$filename" "$album")
if [ -n "$matched_flac_file" ]; then
echo " ✓ Found matching FLAC file: $(basename "$matched_flac_file")"
# Check current LOVE RATING using UTF-8 locale
current_rating=$(run_metaflac --show-tag="LOVE RATING" "$matched_flac_file" 2>/dev/null | cut -d'=' -f2-)
if [ "$current_rating" = "L" ]; then
echo " LOVE RATING already set to 'L'"
else
echo " Setting LOVE RATING to 'L'..."
if set_love_rating "$matched_flac_file" "L"; then
echo " ✓ Successfully updated LOVE RATING"
jellyfin_to_flac=$((jellyfin_to_flac + 1))
else
echo " ✗ Failed to update LOVE RATING (see details above)"
fi
fi
else
echo " ✗ $MATCH_ERROR"
fi
else
echo " ✗ No path available from Jellyfin API, cannot search for file"
fi
echo ""
else
echo "Skipping incomplete entry: Title=[$title] Album=[$album] Path=[$path]"
echo ""
fi
done
fi
echo "============================================"
echo "Script completed successfully!"
echo "============================================"
echo ""
if [[ "$SYNC_DIRECTION" == "both" || "$SYNC_DIRECTION" == "to-jellyfin" ]]; then
echo "FLAC → Jellyfin Results (Sync Type: $FLAC_SYNC_TYPE):"
if [[ "$FLAC_SYNC_TYPE" == "both" || "$FLAC_SYNC_TYPE" == "favorites" ]]; then
echo " 📁 Processed $flac_processed FLAC files for favorites"
if [ -n "$FLAC_LIMIT" ]; then
echo " 📅 Limited to $FLAC_LIMIT most recently modified files for favorites"
fi
echo " ⭐ Marked $flac_to_jellyfin items as favorites in Jellyfin"
else
echo " ⭐ Favorites sync skipped (sync type: $FLAC_SYNC_TYPE)"
fi
echo ""
if [[ "$FLAC_SYNC_TYPE" == "both" || "$FLAC_SYNC_TYPE" == "playcount" ]]; then
echo " 🎵 Processed $playcount_processed FLAC files for play counts"
if [ -n "$PLAYCOUNT_LIMIT" ]; then
echo " 📅 Limited to $PLAYCOUNT_LIMIT most recently modified files for play counts"
fi
echo " 📊 Updated $playcount_updated play counts in Jellyfin"
else
echo " 📊 Play count sync skipped (sync type: $FLAC_SYNC_TYPE)"
fi
echo ""
fi
if [[ "$SYNC_DIRECTION" == "both" || "$SYNC_DIRECTION" == "to-flac" ]]; then
echo "Jellyfin → FLAC Results:"
if [ -n "$SORT_CRITERIA" ] || [ -n "$ITEM_LIMIT" ]; then
echo " 🎯 Used filtering:"
[ -n "$SORT_CRITERIA" ] && echo " - Sort: $SORT_CRITERIA"
[ -n "$ITEM_LIMIT" ] && echo " - Limit: $ITEM_LIMIT items"
fi
echo " ⭐ Processed favorites from Jellyfin"
echo " 📁 Updated $jellyfin_to_flac FLAC files with LOVE RATING = 'L'"
fi
echo ""
echo "Sync completed! Your favorites and play counts are now synchronized."
echo "Debug information saved to /tmp/jellyfin_favorites_debug.json"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment