├── Dockerfile ├── README.md ├── docker.sh └── plex-db-sync /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | MAINTAINER NOSPAM 3 | 4 | COPY docker.sh /docker.sh 5 | COPY plex-db-sync /plex-db-sync 6 | 7 | RUN chmod a+x /docker.sh /plex-db-sync 8 | RUN apk add --update bash sshfs sqlite openssh-client apk-cron && rm -rf /var/cache/apk/* 9 | 10 | CMD ["/docker.sh"] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plex-db-sync 2 | Synchronizes the database watched status between two Plex servers. This includes watched times, and works for all users on the system without the need for tokens. 3 | 4 | ## Usage 5 | To use the script, you will need to be able to access the databases of both Plex servers from one place. This can be done with programs like `sshfs`. For instance, you could run the script like this: 6 | ``` 7 | wget https://raw.githubusercontent.com/Fmstrat/plex-db-sync/master/plex-db-sync 8 | apt-get install sshfs sqlite3 9 | mkdir -p /mnt/sshfs 10 | sshfs -o allow_other,IdentityFile=/keys/serverkey -p 22 \ 11 | root@hostname.tld:"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/" \ 12 | /mnt/sshfs 13 | chmod +x plex-db-sync 14 | ./plex-db-sync \ 15 | --plex-db-1 "/mnt/sshfs/com.plexapp.plugins.library.db" \ 16 | --plex-start-1 "ssh -oStrictHostKeyChecking=no -i /keys/serverkey root@hostname.tld service plexmediaserver start" \ 17 | --plex-stop-1 "ssh -oStrictHostKeyChecking=no -i /keys/serverkey root@hostname.tld service plexmediaserver stop" \ 18 | --plex-db-2 "/data/docker/containers/plex/config/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db" \ 19 | --plex-start-2 "service plexmediaserver start" \ 20 | --plex-stop-2 "service plexmediaserver stop" \ 21 | ``` 22 | The script stops and starts Plex Media Server for a very short period of time to make updates. Due to buffering and reconnections, this does not impact clients when playing, except perhaps on the first run when a very large number of records are being updated. 23 | 24 | ## Docker 25 | The following example is for docker-compose. It assumes you are running one Plex server locally, and another remotely. 26 | ``` 27 | version: '2' 28 | 29 | services: 30 | 31 | plex-db-sync: 32 | image: nowsci/plex-db-sync 33 | container_name: plex-db-sync 34 | volumes: 35 | - /etc/localtime:/etc/localtime:ro 36 | - ./plex-db-sync/sshkey:/sshkey 37 | - /docker/plex/Library/Application Support/Plex Media Server/Plug-in Support/Databases/:/mnt/DB2 38 | cap_add: 39 | - SYS_ADMIN 40 | devices: 41 | - /dev/fuse 42 | security_opt: 43 | - apparmor:unconfined 44 | environment: 45 | - CRON=0 4 * * * 46 | - S1_SSH_KEY=/sshkey 47 | - S1_SSH_USER=root 48 | - S1_SSH_HOST=hostname 49 | - S1_SSH_PORT=22 50 | - S1_SSH_PATH=/docker/plex/Library/Application Support/Plex Media Server/Plug-in Support/Databases 51 | - S1_START=ssh -oStrictHostKeyChecking=no -i /sshkey root@hostname 'cd /docker; docker-compose up -d plex' 52 | - S1_STOP=ssh -oStrictHostKeyChecking=no -i /sshkey root@hostname 'cd /docker; docker-compose stop plex' 53 | - S2_DB_PATH=/mnt/DB2 54 | - S2_START=cd /docker; docker-compose up -d plex 55 | - S2_STOP=cd /docker; docker-compose stop plex 56 | restart: always 57 | ``` 58 | 59 | ## Options 60 | 61 | Command Line | Docker Variable | Description 62 | ------------ | --------------- | ----------- 63 | `--backup ` | `BACKUP` | Create a backup of the DB before running any SQL. 64 | `--debug ` | `DEBUG` | Print debug output. 65 | `--dry-run ` | `DRYRUN` | Don't apply changes to the DB. 66 | `--plex-db-(1/2)` | `S(1/2)_DB_PATH` | Location of the server's DB. For the script, this is the file itself, for docker, it is the path. 67 | `--plex-start-(1/2)` | `S(1/2)_START` | The command to start the Plex server. 68 | `--plex-stop-(1/2)` | `S(1/2)_STOP` | The command to stop the Plex server. 69 | `--nocomparedb ` | n/a | Don't compare version db of Plex server. 70 | n/a | `CRON` | A string that defines when the script should run in crond (Default is 4AM). 71 | n/a | `INITIALRUN` | Run at start prior to starting cron. 72 | n/a | `S(1/2)_SSH_KEY` | The SSH identity file. 73 | n/a | `S(1/2)_SSH_USER` | The SSH user. 74 | n/a | `S(1/2)_SSH_HOST` | The SSH host. 75 | n/a | `S(1/2)_SSH_PORT` | The SSH port. 76 | n/a | `S(1/2)_SSH_PATH` | Path to the database file on the SSH server. 77 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function echoD { 4 | echo "[`date`] ${@}" 5 | } 6 | 7 | function echoDN { 8 | echo -n "[`date`] ${@}" 9 | } 10 | 11 | V_S1_DB_PATH=""; if [ -n "${S1_DB_PATH}" ]; then V_S1_DB_PATH="${S1_DB_PATH}"; fi 12 | V_S2_DB_PATH=""; if [ -n "${S2_DB_PATH}" ]; then V_S2_DB_PATH="${S2_DB_PATH}"; fi 13 | V_BACKUP="false"; if [ -n "${BACKUP}" ]; then V_BACKUP="${BACKUP}"; fi 14 | V_DEBUG="false"; if [ -n "${DEBUG}" ]; then V_DEBUG="${DEBUG}"; fi 15 | V_DRYRUN="false"; if [ -n "${DRYRUN}" ]; then V_DRYRUN="${DRYRUN}"; fi 16 | V_TMPFOLDER="/tmp/plex-db-sync"; if [ -n "${TMPFOLDER}" ]; then V_TMPFOLDER="${TMPFOLDER}"; fi 17 | V_CRON="0 4 * * *"; if [ -n "${CRON}" ]; then V_CRON="${CRON}"; fi 18 | 19 | echo "#!/bin/bash" > /cron-script 20 | echo "" >> /cron-script 21 | 22 | # Set up cron script 23 | if [ -n "${S1_SSH_KEY}" ] && [ -n "${S1_SSH_PORT}" ] && [ -n "${S1_SSH_USER}" ] && [ -n "${S1_SSH_HOST}" ] && [ -n "${S1_SSH_PATH}" ]; then 24 | mkdir -p /mnt/S1 25 | V_S1_DB_PATH="/mnt/S1" 26 | echo -e "echo \x22[\`date\`] Mounting sshfs for server 1...\x22" >> /cron-script 27 | echo -e "sshfs -o allow_other,cache=no,no_readahead,noauto_cache,StrictHostKeyChecking=no,IdentityFile=\x22${S1_SSH_KEY}\x22 -p ${S1_SSH_PORT} ${S1_SSH_USER}@${S1_SSH_HOST}:\x22${S1_SSH_PATH}\x22 /mnt/S1" >> /cron-script 28 | fi 29 | if [ -n "${S2_SSH_KEY}" ] && [ -n "${S2_SSH_PORT}" ] && [ -n "${S2_SSH_USER}" ] && [ -n "${S2_SSH_HOST}" ] && [ -n "${S2_SSH_PATH}" ]; then 30 | mkdir -p /mnt/S2 31 | V_S2_DB_PATH="/mnt/S2" 32 | echo -e "echo \x22[\`date\`] Mounting sshfs for server 2...\x22" >> /cron-script 33 | echo -e "sshfs -o allow_other,cache=no,no_readahead,noauto_cache,StrictHostKeyChecking=no,IdentityFile=\x22${S2_SSH_KEY}\x22 -p ${S2_SSH_PORT} ${S2_SSH_USER}@${S2_SSH_HOST}:\x22${S2_SSH_PATH}\x22 /mnt/S2" >> /cron-script 34 | fi 35 | echo -e "/plex-db-sync --dry-run \x22${V_DRYRUN}\x22 --backup \x22${V_BACKUP}\x22 --debug \x22${V_DEBUG}\x22 --tmp-folder \x22${V_TMPFOLDER}\x22 --plex-db-1 \x22${V_S1_DB_PATH}/com.plexapp.plugins.library.db\x22 --plex-start-1 \x22${S1_START}\x22 --plex-stop-1 \x22${S1_STOP}\x22 --plex-db-2 \x22${V_S2_DB_PATH}/com.plexapp.plugins.library.db\x22 --plex-start-2 \x22${S2_START}\x22 --plex-stop-2 \x22${S2_STOP}\x22 --ignore-accounts \x22${IGNOREACCOUNTS}\x22" >> /cron-script 36 | if [ -n "${S1_SSH_KEY}" ] && [ -n "${S1_SSH_PORT}" ] && [ -n "${S1_SSH_USER}" ] && [ -n "${S1_SSH_HOST}" ] && [ -n "${S1_SSH_PATH}" ]; then 37 | echo "umount /mnt/S1" >> /cron-script 38 | fi 39 | if [ -n "${S2_SSH_KEY}" ] && [ -n "${S2_SSH_PORT}" ] && [ -n "${S2_SSH_USER}" ] && [ -n "${S2_SSH_HOST}" ] && [ -n "${S2_SSH_PATH}" ]; then 40 | echo "umount /mnt/S2" >> /cron-script 41 | fi 42 | chmod +x /cron-script 43 | 44 | if [ "${INITIALRUN}" == "true" ]; then 45 | /cron-script 46 | fi 47 | 48 | # Set up cron 49 | echoD "Setting up cron." 50 | echo -e "${V_CRON} /cron-script" > /crontab.txt 51 | chmod 0644 /crontab.txt 52 | touch /var/log/cron.log 53 | crontab /crontab.txt 54 | crond -f -l 8 55 | -------------------------------------------------------------------------------- /plex-db-sync: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ARGC=$# 4 | ARGV=("$@") 5 | COUNT1=0 6 | COUNT2=0 7 | 8 | function sqlite_query() { 9 | if [ ! -n "$1" ];then 10 | return 1 11 | fi 12 | DB=$1 13 | QUERY=$2 14 | sqlite3 "$DB" "$QUERY" -line -nullvalue "<>" 15 | } 16 | 17 | sqlite_read_line () { 18 | local IFS=" \= " 19 | read COL DATA 20 | local ret=$? 21 | COL_NAME=${COL// /} 22 | return $ret 23 | } 24 | 25 | function sqlite_create_fetch_array() { 26 | result=$(sqlite_query "$1" "$2") 27 | if [[ $? -eq 1 ]]; then 28 | return 1 29 | fi 30 | num_rows=0 31 | unset rows 32 | declare -Ag rows 33 | if [[ $result ]]; then 34 | while sqlite_read_line; do 35 | if [[ ! $COL_NAME ]];then 36 | ((num_rows=num_rows+1)) 37 | fi 38 | rows[$num_rows,"$COL_NAME"]="$DATA" 39 | done <<< "$result" 40 | ((num_rows=num_rows+1)) 41 | fi 42 | } 43 | 44 | function echoD { 45 | echo "[`date`] ${@}" 46 | } 47 | 48 | function echoDN { 49 | echo -n "[`date`] ${@}" 50 | } 51 | 52 | function sql() { 53 | if [ "${DEBUG}" == "true" ]; then 54 | echoD "-=-: $2" 55 | fi 56 | sqlite_create_fetch_array "$1" "$2" 57 | } 58 | 59 | function setnull() { 60 | tmpvar="'${1}'" 61 | if [ "$tmpvar" == "'<>'" ]; then 62 | tmpvar="null" 63 | fi 64 | echo $tmpvar 65 | } 66 | 67 | function initSql() { 68 | if [ -f "${PLEXF1}" ]; then 69 | rm -f "${PLEXF1}" 70 | fi 71 | touch "${PLEXF1}" 72 | if [ -f "${PLEXF2}" ]; then 73 | rm -f "${PLEXF2}" 74 | fi 75 | touch "${PLEXF2}" 76 | } 77 | 78 | function echoSql() { 79 | if [ "${DEBUG}" == "true" ]; then 80 | echoD "${1}: ${2}" 81 | fi 82 | echo "${2}" >> "${1}" 83 | } 84 | 85 | function checkDependencies() { 86 | if [ -z "$(which sqlite3)" ]; then 87 | echoD "Missing dependency: sqlite3" 88 | exit 89 | fi 90 | if [ -z "$(which sshfs)" ]; then 91 | echoD "Missing dependency: sshfs" 92 | exit 93 | fi 94 | } 95 | 96 | function checkForDBs() { 97 | if [ ! -f "$PLEXDB1" ]; then 98 | echoD "Server 1 DB not found" 99 | exit 100 | fi 101 | if [ ! -f "$PLEXDB2" ]; then 102 | echoD "Server 2 DB not found" 103 | exit 104 | fi 105 | if [ "${NOCOMPAREDB}" == "false" ]; then 106 | VPLEXDB1=$(echo "select version from schema_migrations order by version desc limit 1;" | sqlite3 "${PLEXDB1}") 107 | VPLEXDB2=$(echo "select version from schema_migrations order by version desc limit 1;" | sqlite3 "${PLEXDB2}") 108 | if [ "${DEBUG}" == "true" ]; then 109 | echoD "version db1: ${VPLEXDB1} db2: ${VPLEXDB2}" 110 | fi 111 | if [ "${VPLEXDB1}" != "${VPLEXDB2}" ]; then 112 | echoD "Versions of plex databases not equal" 113 | exit 114 | fi 115 | fi 116 | } 117 | 118 | function initFS() { 119 | if [ ! -d "$TMPFOLDER" ]; then 120 | mkdir -p "$TMPFOLDER" 121 | else 122 | if [ -n "${TMPFOLDER}" ]; then 123 | rm -rf "${TMPFOLDER}/"* 124 | fi 125 | fi 126 | MOUNTED=$(mount |grep ramfs |grep "${TMPFOLDER}") 127 | if [ -z "$MOUNTED" ]; then 128 | mount -t ramfs ramfs "$TMPFOLDER" 129 | fi 130 | } 131 | 132 | function closeFS() { 133 | if [ -n "${TMPFOLDER}" ]; then 134 | rm -rf "${TMPFOLDER}/"* 135 | fi 136 | MOUNTED=$(mount |grep ramfs |grep "${TMPFOLDER}") 137 | if [ -n "$MOUNTED" ]; then 138 | umount "${TMPFOLDER}" 139 | fi 140 | } 141 | 142 | function getIgnore() { 143 | IGNORESTR="" 144 | if [ -n "$1" ]; then 145 | OLDIFS=$IFS 146 | IFS="," 147 | for ACCOUNT in $1; do 148 | if [ -z "$IGNORESTR" ]; then 149 | IGNORESTR="${IGNORESTR}lower('${ACCOUNT}')" 150 | else 151 | IGNORESTR="${IGNORESTR},lower('${ACCOUNT}')" 152 | fi 153 | done 154 | IGNORESTR="${IGNORESTR},''" 155 | IFS=$OLDIFS 156 | else 157 | IGNORESTR=''; 158 | fi 159 | echo $IGNORESTR 160 | } 161 | 162 | function createTmpDB() { 163 | TablesTmpDB=(metadata_item_settings metadata_items taggings tags) 164 | for TableTmpDB in ${TablesTmpDB[*]} 165 | do 166 | sqlite3 "${PLEXDB1}" "select sql from sqlite_master where name = '${TableTmpDB}'" | sed -e "s,${TableTmpDB},${TableTmpDB}1,g" | sqlite3 "${TMPDB}" 167 | sqlite3 "${PLEXDB1}" "select sql from sqlite_master where name = '${TableTmpDB}'" | sed -e "s,${TableTmpDB},${TableTmpDB}2,g" | sqlite3 "${TMPDB}" 168 | done 169 | echo "attach '${PLEXDB1}' as plexdb; attach '${TMPDB}' as tmpdb; insert into tmpdb.metadata_item_settings1 select * from plexdb.metadata_item_settings; insert into tmpdb.metadata_items1 select * from plexdb.metadata_items; insert into tmpdb.taggings1 select * from plexdb.taggings; insert into tmpdb.tags1 select * from plexdb.tags;" | sqlite3 170 | echo "attach '${PLEXDB2}' as plexdb; attach '${TMPDB}' as tmpdb; insert into tmpdb.metadata_item_settings2 select * from plexdb.metadata_item_settings; insert into tmpdb.metadata_items2 select * from plexdb.metadata_items; insert into tmpdb.taggings2 select * from plexdb.taggings; insert into tmpdb.tags2 select * from plexdb.tags;" | sqlite3 171 | } 172 | 173 | function processTags() { 174 | # Apply Source and Destinate DBs 175 | if [ "${1}" == "1" ]; then 176 | S=1 177 | D=2 178 | else 179 | S=2 180 | D=1 181 | fi 182 | # Only handling "Share" tags of type 11 right now 183 | # First, let's make sure that any tags needed on the server 184 | sql "${TMPDB}" "select distinct t${S}.* from taggings${S} ts${S}, tags${S} t${S}, metadata_items${S} m${S} where ts${S}.tag_id=t${S}.id and ts${S}.metadata_item_id=m${S}.id and t${S}.tag_type=11 and m${S}.guid in (select guid from metadata_items${D}) and t${S}.tag not in (select tag from tags${D});" 185 | for (( i = 0; i < num_rows; i++ )); do 186 | # Now each one of these tags needs to be inserted. 187 | echoD " - (${i}/${num_rows}) Adding tag ${rows[$i,"tag"]} on server ${D}" 188 | echoSql "${2}" "insert into tags (metadata_item_id, tag, tag_type, user_thumb_url, user_art_url, user_music_url, created_at, updated_at, tag_value, extra_data, key, parent_id) values (`setnull "${rows[$i,"metadata_item_id"]}"`, `setnull "${rows[$i,"tag"]}"`, `setnull "${rows[$i,"tag_type"]}"`, `setnull "${rows[$i,"user_thumb_url"]}"`, `setnull "${rows[$i,"user_art_url"]}"`, `setnull "${rows[$i,"user_music_url"]}"`, `setnull "${rows[$i,"created_at"]}"`, `setnull "${rows[$i,"updated_at"]}"`, `setnull "${rows[$i,"tag_value"]}"`, `setnull "${rows[$i,"extra_data"]}"`, `setnull "${rows[$i,"key"]}"`, `setnull "${rows[$i,"parent_id"]}"`);"; 189 | if [ "${1}" == "1" ]; then 190 | (( COUNT2++ )); 191 | else 192 | (( COUNT1++ )); 193 | fi 194 | done 195 | # Now we can insert any taggings required 196 | sql "${TMPDB}" "select ts${S}.*, t${S}.tag, m${S}.guid from taggings${S} ts${S}, tags${S} t${S}, metadata_items${S} m${S} where ts${S}.tag_id=t${S}.id and ts${S}.metadata_item_id=m${S}.id and t${S}.tag_type=11 and m${S}.guid in (select guid from metadata_items${D}) and m${S}.guid not in (select distinct m${D}.guid from taggings${D} ts${D}, tags${D} t${D}, metadata_items${D} m${D} where ts${D}.tag_id=t${D}.id and ts${D}.metadata_item_id=m${D}.id and t${D}.tag_type=11 and t${D}.tag=t${S}.tag);" 197 | for (( i = 0; i < num_rows; i++ )); do 198 | echoD " - (${i}/${num_rows}) Adding tag ${rows[$i,"tag"]} to ${rows[$i,"guid"]} on server ${D}" 199 | echoSql "${2}" "insert into taggings (metadata_item_id, tag_id, 'index', text, time_offset, end_time_offset, thumb_url, created_at, extra_data) select m.id as metadata_item_id, t.id as tag_id, `setnull "${rows[$i,"index"]}"`, `setnull "${rows[$i,"text"]}"`, `setnull "${rows[$i,"time_offset"]}"`, `setnull "${rows[$i,"end_time_offset"]}"`, `setnull "${rows[$i,"thumb_url"]}"`, `setnull "${rows[$i,"created_at"]}"`, `setnull "${rows[$i,"extra_data"]}"` from metadata_items m, tags t where m.guid='${rows[$i,"guid"]}' and t.tag='${rows[$i,"tag"]}';" 200 | if [ "${1}" == "1" ]; then 201 | (( COUNT2++ )); 202 | else 203 | (( COUNT1++ )); 204 | fi 205 | done 206 | } 207 | 208 | function processWatchedCommon() { 209 | sql "${TMPDB}" "select s1.guid, s1.id as id1, s1.updated_at as updated_at1, s1.view_count as view_count1, s1.last_viewed_at as last_viewed_at1, s1.view_offset as view_offset1, s1.changed_at as changed_at1, s2.id as id2, s2.updated_at as updated_at2, s2.view_count as view_count2, s2.last_viewed_at as last_viewed_at2, s2.view_offset as view_offset2, s2.changed_at as changed_at2 from metadata_item_settings1 s1, metadata_item_settings2 s2 where s1.guid=s2.guid and s1.account_id=s2.account_id and s1.account_id=${1} and (s1.view_count != s2.view_count or s1.last_viewed_at != s2.last_viewed_at or s1.view_offset != s2.view_offset or s1.changed_at != s2.changed_at);" 210 | for (( i = 0; i < num_rows; i++ )); do 211 | # Which record is newer? 212 | updated_at1_int=$(date -d "${rows[$i,"updated_at1"]}" +%s) 213 | updated_at2_int=$(date -d "${rows[$i,"updated_at2"]}" +%s) 214 | if [ ${updated_at1_int} -ge ${updated_at2_int} ] || [ ${rows[$i,"changed_at1"]} -ge ${rows[$i,"changed_at2"]} ]; then 215 | # Server 1 is newer 216 | echoD " - (${i}/${num_rows}) Setting server 2 status for: ${rows[$i,"guid"]}" 217 | last_viewed_at=$(setnull "${rows[$i,"last_viewed_at1"]}") 218 | view_offset=$(setnull "${rows[$i,"view_offset1"]}") 219 | echoSql "${PLEXF2}" "update metadata_item_settings set updated_at='${rows[$i,"updated_at1"]}', view_count=${rows[$i,"view_count1"]}, last_viewed_at=${last_viewed_at}, view_offset=${view_offset}, changed_at=${rows[$i,"changed_at1"]} where id=${rows[$i,"id2"]};" 220 | ((COUNT2++)) 221 | else 222 | # Server 2 is newer 223 | echoD " - (${i}/${num_rows}) Setting server 1 status for: ${rows[$i,"guid"]}" 224 | last_viewed_at=$(setnull "${rows[$i,"last_viewed_at2"]}") 225 | view_offset=$(setnull "${rows[$i,"view_offset2"]}") 226 | echoSql "${PLEXF1}" "update metadata_item_settings set updated_at='${rows[$i,"updated_at2"]}', view_count=${rows[$i,"view_count2"]}, last_viewed_at=${last_viewed_at}, view_offset=${view_offset}, changed_at=${rows[$i,"changed_at2"]} where id=${rows[$i,"id1"]};" 227 | (( COUNT1++ )); 228 | fi 229 | done 230 | } 231 | 232 | function processWatchedNew() { 233 | sql "${TMPDB}" "select * from metadata_item_settings${3} where guid in (select guid from metadata_items${4}) and guid not in (select guid from metadata_item_settings${4} where account_id=${1}) and account_id=${1};" 234 | for (( i = 0; i < num_rows; i++ )); do 235 | echoD " - (${i}/${num_rows}) Setting server ${4} status for: ${rows[$i,"guid"]}" 236 | rating=$(setnull "${rows[$i,"rating"]}") 237 | view_offset=$(setnull "${rows[$i,"view_offset"]}") 238 | last_viewed_at=$(setnull "${rows[$i,"last_viewed_at"]}") 239 | skip_count=$(setnull "${rows[$i,"skip_count"]}") 240 | last_skipped_at=$(setnull "${rows[$i,"last_skipped_at"]}") 241 | extra_data=$(setnull "${rows[$i,"extra_data"]}") 242 | echoSql "${2}" "insert into metadata_item_settings (account_id, guid, rating, view_offset, view_count, last_viewed_at, created_at, updated_at, skip_count, last_skipped_at, changed_at, extra_data) values ('${1}', '${rows[$i,"guid"]}', ${rating}, ${view_offset}, '${rows[$i,"view_count"]}', ${last_viewed_at}, '${rows[$i,"created_at"]}', '${rows[$i,"updated_at"]}', ${skip_count}, ${last_skipped_at}, ${rows[$i,"changed_at"]}, ${extra_data});" 243 | if [ ${4} -eq 2 ]; then 244 | ((COUNT2++)) 245 | else 246 | ((COUNT1++)) 247 | fi 248 | done 249 | } 250 | 251 | function getConfig() { 252 | V="" 253 | for (( j=0; j /dev/null 2>&1 278 | echo "Done" 279 | fi 280 | if [ -n "${PLEXSTOP2}" ]; then 281 | echoDN "Stopping Plex on Server 2... " 282 | eval ${PLEXSTOP2} 1> /dev/null 2>&1 283 | echo "Done" 284 | fi 285 | sleep 3 286 | } 287 | 288 | function startServers() { 289 | sleep 3 290 | if [ -n "${PLEXSTART1}" ]; then 291 | echoDN "Starting Plex on Server 1... " 292 | eval ${PLEXSTART1} 1> /dev/null 2>&1 293 | echo "Done" 294 | fi 295 | if [ -n "${PLEXSTART2}" ]; then 296 | echoDN "Starting Plex on Server 2... " 297 | eval ${PLEXSTART2} 1> /dev/null 2>&1 298 | echo "Done" 299 | fi 300 | } 301 | 302 | function updateServers() { 303 | if [ $COUNT1 -gt 0 ]; then 304 | if [ "${BACKUP}" == "true" ]; then 305 | runBackup "${PLEXDB1}" 306 | fi 307 | file2sql "${PLEXDB1}" "${PLEXF1}" 308 | fi 309 | if [ $COUNT2 -gt 0 ]; then 310 | if [ "${BACKUP}" == "true" ]; then 311 | runBackup "${PLEXDB2}" 312 | fi 313 | file2sql "${PLEXDB2}" "${PLEXF2}" 314 | fi 315 | } 316 | 317 | function file2sql() { 318 | if [ "$DRYRUN" == false ]; then 319 | echoD "Applying DB changes to ${1}..." 320 | sqlite3 "$1" < "$2" 321 | else 322 | echoD "(DRY RUN) Applying DB changes to ${1}..." 323 | fi 324 | } 325 | 326 | function runBackup() { 327 | echoD "Backing up ${1}..." 328 | cp -a "${1}" "${1}.dbsyc.bak" 329 | } 330 | 331 | function printConfig() { 332 | if [ "${DEBUG}" == "true" ]; then 333 | echoD "TMPFOLDER: ${TMPFOLDER}" 334 | echoD "DEBUG: ${DEBUG}" 335 | echoD "PLEXDB1: ${PLEXDB1}" 336 | echoD "PLEXDB2: ${PLEXDB2}" 337 | echoD "PLEXSTART1: ${PLEXSTART1}" 338 | echoD "PLEXSTOP1: ${PLEXSTOP1}" 339 | echoD "PLEXSTART2: ${PLEXSTART2}" 340 | echoD "PLEXSTOP2: ${PLEXSTOP2}" 341 | echoD "PLEXF1: ${PLEXF1}" 342 | echoD "PLEXF2: ${PLEXF2}" 343 | echoD "TMPDB: ${TMPDB}" 344 | echoD "IGNORE: ${IGNORE}" 345 | echoD "IGNORESTR: ${IGNORESTR}" 346 | echoD "BACKUP: ${BACKUP}" 347 | fi 348 | } 349 | 350 | function showUsage() { 351 | echoD "Usage: ./plex-db-sync.sh --dry-run --backup --debug --nocomparedb --plex-db-1 --plex-db-2 --plex-start-1 --plex-stop-1 --plex-start-2 --plex-stop-2 --ignore-accounts --tmp-folder " 352 | } 353 | 354 | function checkForNew() { 355 | #return 0 356 | UP1=$(echo "select updated_at from metadata_item_settings order by updated_at desc limit 1;" | sqlite3 "${PLEXDB1}") 357 | UP2=$(echo "select updated_at from metadata_item_settings order by updated_at desc limit 1;" | sqlite3 "${PLEXDB2}") 358 | if [ "$DEBUG" == "true" ]; then 359 | echoD "UP1: $UP1 - UP2: $UP2" 360 | fi 361 | if [ "${UP1}" != "${UP2}" ]; then 362 | return 0 363 | fi 364 | CH1=$(echo "select changed_at from metadata_item_settings order by changed_at desc limit 1;" | sqlite3 "${PLEXDB1}") 365 | CH2=$(echo "select changed_at from metadata_item_settings order by changed_at desc limit 1;" | sqlite3 "${PLEXDB2}") 366 | if [ "$DEBUG" == "true" ]; then 367 | echoD "CH1: $CH1 - CH2: $CH2" 368 | fi 369 | if [ "${CH1}" != "${CH2}" ]; then 370 | return 0 371 | fi 372 | return 1 373 | } 374 | 375 | echoD "Starting." 376 | checkDependencies 377 | TMPFOLDER=$(getConfig "--tmp-folder" "/tmp/plex-db-sync") 378 | DEBUG=$(getConfig "--debug" "false") 379 | BACKUP=$(getConfig "--backup" "false") 380 | DRYRUN=$(getConfig "--dry-run" "false") 381 | NOCOMPAREDB=$(getConfig "--nocomparedb" "false") 382 | PLEXDB1=$(getConfig "--plex-db-1" "") 383 | PLEXDB2=$(getConfig "--plex-db-2" "") 384 | PLEXSTART1=$(getConfig "--plex-start-1" "") 385 | PLEXSTOP1=$(getConfig "--plex-stop-1" "") 386 | PLEXSTART2=$(getConfig "--plex-start-2" "") 387 | PLEXSTOP2=$(getConfig "--plex-stop-2" "") 388 | IGNORE=$(getConfig "--ignore-accounts" "") 389 | PLEXF1="${TMPFOLDER}/1.sql" 390 | PLEXF2="${TMPFOLDER}/2.sql" 391 | TMPDB="${TMPFOLDER}/tmp.db" 392 | IGNORESTR=$(getIgnore "$IGNORE") 393 | checkRequired "--plex-db-1" 394 | checkRequired "--plex-db-2" 395 | checkRequired "--plex-start-1" 396 | checkRequired "--plex-start-2" 397 | checkRequired "--plex-stop-1" 398 | checkRequired "--plex-stop-2" 399 | printConfig 400 | checkForDBs 401 | 402 | stopServers 403 | echoDN "Checking for changes... " 404 | if checkForNew; then 405 | echo "Found" 406 | initFS 407 | initSql 408 | createTmpDB 409 | 410 | echoD "Processing tags..." 411 | processTags 1 "${PLEXF2}" 412 | processTags 2 "${PLEXF1}" 413 | 414 | # Get accounts on server 1 415 | sql "$PLEXDB1" "select id, name from accounts where lower(name) not in (${IGNORESTR}) order by id;" 416 | rowstr=$(declare -p rows) 417 | eval "declare -A accounts1="${rowstr#*=} 418 | num_accounts1=$num_rows 419 | 420 | # Get accounts on server 2 421 | sql "$PLEXDB2" "select id, name from accounts where lower(name) not in (${IGNORESTR}) order by id;" 422 | rowstr=$(declare -p rows) 423 | eval "declare -A accounts2="${rowstr#*=} 424 | num_accounts2=$num_rows 425 | 426 | for (( a1 = 0; a1 < num_accounts1; a1++ )); do 427 | for (( a2 = 0; a2 < num_accounts2; a2++ )); do 428 | # If a match, process 429 | if [ "${accounts1[$a1,"id"]}" == "${accounts2[$a2,"id"]}" ]; then 430 | echoD "Processing for ${accounts1[$a1,"name"]} (${accounts1[$a1,"id"]})..." 431 | 432 | # Start with records that are in both tables 433 | echoD " - Checking records that are in both databases" 434 | processWatchedCommon ${accounts1[$a1,"id"]} 435 | 436 | # Then look at records that are on one server but not the other 437 | # We will start with server 1 438 | echoD " - Checking records missing from server 2" 439 | processWatchedNew ${accounts1[$a1,"id"]} "${PLEXF2}" 1 2 440 | 441 | # And then server 2 442 | echoD " - Checking records missing from server 1" 443 | processWatchedNew ${accounts1[$a1,"id"]} "${PLEXF1}" 2 1 444 | fi 445 | done 446 | done 447 | 448 | updateServers 449 | closeFS 450 | else 451 | echo "None found" 452 | fi 453 | startServers 454 | echoD "Finished." 455 | --------------------------------------------------------------------------------