Use hashes instead of paths
authorCameron Ball <cameron@moodle.com>
Wed, 13 Mar 2019 08:49:31 +0000 (16:49 +0800)
committerCameron Ball <cameron@cameron1729.xyz>
Sun, 31 Mar 2019 12:45:09 +0000 (20:45 +0800)
.gitignore
FaveSync.sh
PathsToMd5.sh [new file with mode: 0755]
ScoreSync.sh
SongSync.sh [new file with mode: 0755]
pull.sh [new file with mode: 0755]

index cdd7c19..260c213 100644 (file)
@@ -1 +1,2 @@
 config.sh
+*.txt
index 5036f28..b9d7d53 100755 (executable)
@@ -9,34 +9,41 @@ fi
 
 if [ "$1" = "push" ]; then
     if find "$path_to_songs/$faves_folder" -mindepth 1 -print -quit 2>/dev/null | grep -q .; then
-        find "$path_to_songs/$faves_folder" -type l -print0 | xargs --null -n1 readlink | rev | cut -sd / -f2,3 | rev > "$DIR/$me.favourites.txt"
-        scp "$DIR/$me.favourites.txt" groovenet@cameron1729.xyz:/mnt/media/GrooveNet
+        find -L "$path_to_songs/$faves_folder" -name \*.sm -exec md5sum {} \; | cut -sd ' ' -f1 > "$DIR/$me.favourites.txt"
+        #scp "$DIR/$me.favourites.txt" groovenet@cameron1729.xyz:/mnt/media/GrooveNet
     fi
 fi
 
-if [ "$1" = "pull" ]; then
-    scp groovenet@cameron1729.xyz:/mnt/media/GrooveNet/* "$DIR"
-fi
-
 if [ "$1" = "apply" ]; then
     if find "${DIR}"/*.favourites.txt -type f -print -quit 2>/dev/null | grep -q .; then
         find "${DIR}"/*.favourites.txt -type f -print0 | while read -d $'\0' file; do
             file=$(basename "$file")
 
-            if [ "$file" != "${me}.favourites.txt" ]; then
-                IFS=$'\n' read -d '' -r -a lines < $file
-                person="${file%.*.*}"
-                person="$(tr '[:lower:]' '[:upper:]' <<< ${person:0:1})${person:1}"
-                persons_folder=${external_favourite_naming_scheme/\%person\%/$person}
+            IFS=$'\n' read -d '' -r -a lines < $file
+            person="${file%.*.*}"
 
-                rm -rf "$path_to_songs/$persons_folder"
-                mkdir "$path_to_songs/$persons_folder"
-
-                for i in "${lines[@]}"; do
-                    just_song=$(echo $i | cut -sd / -f2)
-                    ln -s "$path_to_songs/$i" "$path_to_songs/$persons_folder/$just_song"
-                done
+            if [ "$person" = "$me" ]; then
+                persons_folder="$faves_folder"
+            else
+                formatted_person="$(tr '[:lower:]' '[:upper:]' <<< ${person:0:1})${person:1}"
+                persons_folder=${external_favourite_naming_scheme/\%person\%/$formatted_person}
             fi
+
+            rm -rf "$path_to_songs/$persons_folder"
+            mkdir "$path_to_songs/$persons_folder"
+
+            pattern=""
+            for i in "${lines[@]}"; do
+                pattern="${pattern}|$i"
+            done
+
+            pattern="${pattern:1}"
+            song_paths=$(grep -E "$pattern" songs.txt | sort -u -k1,1 | cut -sd / -f5-6)
+
+            while read -r song_path; do
+                just_song=$(echo $song_path | cut -sd / -f2)
+                ln -s "$path_to_songs/$song_path" "$path_to_songs/$persons_folder/$just_song"
+            done <<< "$song_paths"
         done
     fi
 fi
diff --git a/PathsToMd5.sh b/PathsToMd5.sh
new file mode 100755 (executable)
index 0000000..074f82d
--- /dev/null
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+songs_file=$(ls $DIR | grep songs | tail -n1)
+
+if [ -z "$songs_file" ]; then
+    exit
+fi
+
+while read -d $'\0' file; do
+    IFS=$'\n' read -d '' -r -a lines < $file
+    rm "${file}.md5"
+    echo "${lines[0]}" > "${file}.md5"
+    for i in "${lines[@]:1}"; do
+        person_and_folder=$(echo $i | cut -sd ':' -f1)
+        person=$(echo $person_and_folder | cut -sd / -f1)
+        folder=$(echo $person_and_folder | cut -sd / -f2)
+        folder=$(echo $folder | sed 's/\&amp;/\&/g;')
+        full_folder=$(cat "$person.favourites.txt" | grep -F "$folder")
+        song_difficulty=$(echo "$i" | cut -sd : -f2)
+        song_steps=$(echo "$i" | cut -sd : -f3)
+        song_score=$(echo "$i" | cut -sd : -f4)
+
+        if ! [ -z "$full_folder" ]; then
+            hash=$(grep -F "${full_folder}/" $songs_file | cut -sd ' ' -f1)
+            echo "${hash}:${song_difficulty}:${song_steps}:${song_score}" >> "${file}.md5"
+        else
+            echo "Folder not found for $i"
+        fi
+    done
+done < <(find "${DIR}"/*.scores.txt -type f -print0)
+
+while read -d $'\0' file;do
+    IFS=$'\n' read -d '' -r -a lines < $file
+    pattern=""
+    for i in "${lines[@]}"; do
+        pattern="${pattern}/|$i"
+    done
+    pattern="${pattern:2}"
+    pattern=$(echo $pattern | sed 's/\[/\\[/g; s/\]/\\]/g; s/)/\\)/g; s/(/\\(/g; s/+/\\+/g')
+    grep -E "$pattern" $songs_file | cut -sd ' ' -f1 > "${file}.md5"
+done < <(find "${DIR}"/*.favourites.txt -type f -print0)
index d33de88..97898f6 100755 (executable)
@@ -7,12 +7,50 @@ if [ -z $1 ]; then
     exit
 fi
 
+songs_file=$(ls $DIR | grep songs | tail -n1)
+
+if [ -z "$songs_file" ]; then
+    exit
+fi
+
 sed -i '1s/.*/<?xml version="1.0" encoding="ISO-8859-1" ?>/' "${path_to_stats}"
 difficulties=(Challenge Edit Hard Medium)
 step_types=("dance-double" "dance-single")
 tiers=("1.00" "0.99" "0.98" "0.96" "0.94" "0.92" "0.89" "0.86" "0.83" "0.80" "0.76" "0.72" "0.68" "0.64" "0.60" "0.55")
 
 if [ "$1" = "push" ]; then
+    declare -A all_song_hashes
+    declare -A hash_map
+
+    # pretty sure this can be merged with the shit below somehow
+    if find "${DIR}"/*.favourites.txt -type f -print -quit 2>/dev/null | grep -q .; then
+        pattern=""
+        while read -d $'\0' file; do
+            IFS=$'\n' read -d '' -r -a lines < $file
+            basename=$(basename "$file")
+            person="${basename%.*.*}"
+            if [ "$person" = "$me" ]; then
+                persons_folder="$faves_folder"
+            else
+                formatted_person="$(tr '[:lower:]' '[:upper:]' <<< ${person:0:1})${person:1}"
+                persons_folder=${external_favourite_naming_scheme/\%person\%/$formatted_person}
+            fi
+            for i in "${lines[@]}"; do
+                all_song_hashes["$i"]+="$persons_folder"
+                pattern="${pattern}|$i"
+            done
+        done < <(find "${DIR}"/*.favourites.txt -type f -print0)
+    fi
+
+    pattern="${pattern:1}"
+    song_paths=$(grep -E "$pattern" $songs_file | sort -u -k1,1)
+
+    while read -r song_path; do
+        hash=$(echo $song_path | cut -sd ' ' -f1)
+        path=$(echo $song_path | cut -sd / -f6 )
+        hash_map["${all_song_hashes[$hash]}/$path"]+="$hash"
+    done <<< "$song_paths"
+
     if find "${DIR}"/*.favourites.txt -type f -print -quit 2>/dev/null | grep -q .; then
         while read -d $'\0' file; do
             file=$(basename "$file")
@@ -26,63 +64,234 @@ if [ "$1" = "push" ]; then
                 persons_folder=${external_favourite_naming_scheme/\%person\%/$formatted_person}
             fi
 
-
-            for d in "${difficulties[@]}"; do
-                for t in "${step_types[@]}"; do
-                    xmlstarletcmd="$xmlstarletcmd -o \"NEWLIST:$person:$t:$d\" -n \
-                                 -v \"//SongScores//Song[starts-with(@Dir, \\\"Songs/$persons_folder\\\")]//Steps[@Difficulty=\\\"$d\\\" and @StepsType=\\\"$t\\\"]//HighScore[1]//Name[text()=\\\"EVNT\\\" or text()=\\\"$me_highscore_name\\\"]/following-sibling::PercentDP\" \
-                                 -n -o \"SONGS\" -n \
-                                 -v \"//SongScores//Song[starts-with(@Dir, \\\"Songs/$persons_folder\\\")]//Steps[@Difficulty=\\\"$d\\\" and @StepsType=\\\"$t\\\"]//HighScore[1]//Name[text()=\\\"EVNT\\\" or text()=\\\"$me_highscore_name\\\"]/ancestor::Song/@Dir\""
-                done
-            done
+            xmlstarlet_meta_cmd="$xmlstarlet_meta_cmd -v \"//SongScores//Song[starts-with(@Dir, \\\"Songs/${persons_folder}\\\")][Steps//PercentDP]//@*\" -n"
+            xmlstarlet_scores_cmd="$xmlstarlet_scores_cmd -o \"SCORES\" -n -v \"//SongScores//Song[starts-with(@Dir, \\\"Songs/${persons_folder}\\\")]//PercentDP\" -n -o \"NAMES\" -n -v \"//SongScores/Song[starts-with(@Dir, \\\"Songs/${persons_folder}\\\")]//Name\" -n"
         done < <(find "${DIR}"/*.favourites.txt -type f -print0)
 
-        scores_and_shit=$(eval "xmlstarlet sel -t $xmlstarletcmd -n -o \"NEWLIST\" \"${path_to_stats}\"")
-        IFS=$'\n' read -rd '' -a scores_list <<<"$scores_and_shit"
+        meta=$(eval "xmlstarlet sel -I -t $xmlstarlet_meta_cmd -o \"Songs\" \"${path_to_stats}\"")
+        scores=$(eval "xmlstarlet sel -I -t $xmlstarlet_scores_cmd \"${path_to_stats}\"")
+        nulls=$(eval "xmlstarlet sel -t -v \"//LastPlayed[text()=\\\"1933-01-02\\\"]/ancestor::Song/@Dir\" \"${path_to_stats}\"")
+
+        declare -a null_hashes
+
+        while read -r null; do
+            the_path=$(echo "$null" | cut -sd / -f2,3 | sed 's/&amp;/\&/g; s/&lt;/\</g; s/&gt;/\>/g; s/&quot;/\"/g; s/&apos;/\'"'"'/g')
+            null_hashes+=("${hash_map[$the_path]}")
+        done <<< "$nulls"
 
-        echo "$me_highscore_name" > "${DIR}/$me.scores.txt"
-        declare -a scores
-        declare -a folders
-        for i in "${scores_list[@]}"; do
-            if [[ ${i:0:7} = "NEWLIST" ]]; then
-                for j in "${!scores[@]}"; do
-                    write_out="${folders[$j]}:${d}:${t}:${scores[$j]}\n$write_out"
+        new_song=false
+        song_hash=""
+        declare -a d
+        declare -a t
+        counter=0
+        bigboi_counter=0
+        declare -A mmeta
+        declare -a sscores
+        declare -a names
+
+        scores_or_names="SCORES"
+
+        while read -r s; do
+            if [ "$s" = "SCORES" ]; then
+                scores_or_names="SCORES"
+                continue
+            fi
+
+            if [ "$s" = "NAMES" ]; then
+                scores_or_names="NAMES"
+                continue
+            fi
+
+            if ! [ "$scores_or_names" = "SCORES" ]; then
+                sscores+=($s)
+                continue
+            fi
+
+            if ! [ "$scores_or_names" = "NAMES" ]; then
+                names+=($s)
+                continue
+            fi
+        done <<< "$scores"
+
+        while read -r m; do
+            if [[ ${m:0:5} = "Songs" ]]; then
+                for dd in "${!d[@]}"; do
+                    if ! [ -z "${mmeta["${song_hash}:${d[$dd]}:${t[$dd]}"]}" ]; then
+                        echo "!!!! that key exists already..."
+                    fi
+
+                    mmeta["${song_hash}:${d[$dd]}:${t[$dd]}"]="${names[$bigboi_counter]}:${sscores[$bigboi_counter]}"
+                    bigboi_counter=$((bigboi_counter+1))
                 done
 
-                scores=()
-                folders=()
+                d=()
+                t=()
+                counter=0
+                if [[ ${m:0:6} = "Songs/" ]]; then
+                    plain_song=$(echo $m | sed 's/&amp;/\&/g;' | cut -sd / -f2,3)
+                    song_hash=${hash_map[$plain_song]}
+                fi
+                continue
+            fi
+
+            if (( $counter % 2 )); then
+                d+=($m)
+            else
+                t+=($m)
+            fi
+            counter=$((counter+1))
+        done <<< "$meta"
+
+        # this is a list of all hashes and who has the score on them
+        # todo: go over this list, and apply the following:
+        # remove the hash from all_song_hashes and:
+        # if I have a score on it, add it to writeout
+        # if someone else has the score on it add it to writeout with NODATA
+        # if it is in null nodes (need to write code for this) add it to writeout with NODATA
+        # all_song_hashes should now have the information we need to retrieve the song from elsewhere in stats.xml
+        writeout=""
+        for fuck in "${!mmeta[@]}"; do
+            the_score=$(echo ${mmeta[$fuck]} | cut -sd : -f1)
+            the_scorer=$(echo ${mmeta[$fuck]} | cut -sd : -f2)
+            just_hash=$(echo $fuck | cut -sd : -f1)
+
+            if [ "$the_scorer" = "$me_highscore_name" ]; then
+                writeout="$writeout\n${fuck}:${the_score}"
+            else
+                writeout="$writeout\n${just_hash}:NODATA"
+            fi
+            unset all_song_hashes["$just_hash"]
+            #echo "$fuck $the_score $the_scorer"
+        done
+
+        for shit in "${null_hashes[@]}"; do
+            unset all_song_hashes["$shit"]
+            writeout="$writeout\n${shit}:NODATA"
+        done
+
+        # now all that's needed is to locate the data for these nodes (they are ones that were faved this session, so cannot exist in stats.xml for the remainder of the sesh)
+        # TODO: Technically these queries are not quite specific enough. They could find scores left by players other than the machine owner
+        # but probably it's fine
+        for fuck in "${!all_song_hashes[@]}"; do
+            pathh=$(cat $songs_file | grep $fuck | cut -sd / -f5,6 )
+            xmlstarlet_orphans_cmd="$xmlstarlet_orphans_cmd -i \"//SongScores//Song[@Dir=\\\"Songs/${pathh}/\\\"]//Steps//HighScore[1]//PercentDP\" \
+                                 -o \"SONG:${fuck}\" -n \
+                                 -o \"STEPTYPES\" -n \
+                                 -v \"//SongScores//Song[@Dir=\\\"Songs/${pathh}/\\\"]//Steps//HighScore//PercentDP/ancestor::Steps/@StepsType\" -n \
+                                 -o \"DIFFICULTIES\" -n \
+                                 -v \"//SongScores//Song[@Dir=\\\"Songs/${pathh}/\\\"]//Steps//HighScore//PercentDP/ancestor::Steps/@Difficulty\" -n \
+                                 -o \"SCORES\" -n \
+                                 -v \"//SongScores//Song[@Dir=\\\"Songs/${pathh}/\\\"]//Steps//HighScore[1]//PercentDP\" -n \
+                                 --else \
+                                 -o \"SONG:${fuck}\" -n \
+                                 -o \"NOSCORES\" -n \
+                                 --break"
+        done
+
+        scores_living_elsewhere=$(eval "xmlstarlet sel -t $xmlstarlet_orphans_cmd -o \"SONG\" \"${path_to_stats}\"")
+        IFS=$'\n' read -rd '' -a scores_elsewhere <<<"$scores_living_elsewhere"
+
+        declare -a jjj_types
+        declare -a jjj_diffs
+        declare -a jjj_scores
+        past_steptypes=false
+        past_difficulties=false
+        past_scores=false
+        no_dump=false
+        for i in "${scores_elsewhere[@]}"; do
+            if [[ ${i:0:4} = "SONG" ]]; then
+                if [ "$no_dump" = false ]; then
+                    for j in "${!jjj_types[@]}"; do
+                        writeout="${writeout}\n${jjj_hash}:${jjj_types[$j]}::${jjj_diffs[$j]}:${jjj_scores[$j]}"
+                    done
+                else
+                    writeout="${writeout}\n${jjj_hash}:NODATA"
+                fi
+
+                jjj_types=()
+                jjj_diffs=()
+                jjj_scores=()
+                past_steptypes=false
+                past_difficulties=false
                 past_scores=false
-                p=$(echo $i | cut -sd : -f2)
-                t=$(echo $i | cut -sd : -f3)
-                d=$(echo $i | cut -sd : -f4)
+                no_dump=false
+                jjj_hash=$(echo $i | cut -sd ':' -f2)
+                continue
+            fi
 
+            if [ "$i" = "NOSCORES" ]; then
+                no_dump=true
                 continue
             fi
 
-            if [ "$i" = "SONGS" ]; then
+            if [ "$i" = "STEPTYPES" ]; then
+                past_steptypes=true
+                continue
+            fi
+
+            if [ "$i" = "DIFFICULTIES" ]; then
+                past_difficulties=true
+                continue
+            fi
+
+            if [ "$i" = "SCORES" ]; then
                 past_scores=true
                 continue
             fi
 
             if [ "$past_scores" = true ]; then
-                song_dir=$(echo $i | rev | cut -sd / -f2 | rev)
-                folders+=("${p}/${song_dir}")
-            else
-                scores+=("$i")
+                jjj_scores+=("$i")
+                continue
+            fi
+
+            if [ "$past_difficulties" = true ]; then
+                jjj_diffs+=("$i");
+                continue
             fi
-        done
 
-        echo -e "$write_out" >> "${DIR}/$me.scores.txt"
-        scp "$DIR/$me.scores.txt" groovenet@cameron1729.xyz:/mnt/media/GrooveNet
+            if [ "$past_steptypes" = true ]; then
+                jjj_types+=("$i")
+                continue
+            fi
+        done
+        echo -e "$writeout"
+        #scp "$DIR/$me.scores.txt" groovenet@cameron1729.xyz:/mnt/media/GrooveNet
     fi
 fi
 
 if [ "$1" = "apply" ]; then
+    declare -A all_song_hashes
     declare -A scores
     declare -a all_scores
     declare -a bros
     declare -A pranks
+    declare -A hash_map
+    declare -A null_files
     xmlstarlet_deletes=""
+    xmlstarlet_orphans_cmd=""
+
+    pattern=""
+    if find "${DIR}"/*.favourites.txt -type f -print -quit 2>/dev/null | grep -q .; then
+        while read -d $'\0' file; do
+            IFS=$'\n' read -d '' -r -a lines < $file
+            basename=$(basename "$file")
+            person="${basename%.*.*}"
+            for i in "${lines[@]}"; do
+                all_song_hashes["$i"]+="$person"
+                pattern="${pattern}|$i"
+            done
+        done < <(find "${DIR}"/*.favourites.txt -type f -print0)
+    fi
+
+    pattern="${pattern:1}"
+    song_paths=$(grep -E "$pattern" $songs_file | sort -u -k1,1)
+
+    while read -r song_path; do
+        hash=$(echo $song_path | cut -sd ' ' -f1)
+        path=$(echo $song_path | cut -sd / -f5-6 )
+        hash_map["$hash"]+="$path"
+    done <<< "$song_paths"
+
     if find "${DIR}"/*.scores.txt -type f -print -quit 2>/dev/null | grep -q .; then
         while read -d $'\0' file; do
             basename=$(basename "$file")
@@ -101,39 +310,168 @@ if [ "$1" = "apply" ]; then
             highscorename=${lines[0]}
             bros+=("$highscorename")
             for i in "${lines[@]:1}"; do
-                song_score=$(echo $i | rev | cut -sd : -f1 | rev)
-                song_steps_type=$(echo $i | rev | cut -sd : -f2 | rev)
-                song_difficulty=$(echo $i | rev | cut -sd : -f3 | rev)
-                song_path=$(echo $i | rev | cut -sd : -f4 | rev)
-                scores["${highscorename}:${song_path}:${song_difficulty}:${song_steps_type}"]+="${song_score}"
-                all_scores+=("${song_path}:${song_difficulty}:${song_steps_type}")
+                if [ "$(echo $i | cut -sd : -f2)" = "NODATA" ]; then
+                    # null files is used to store whether or not an attempt to copy this song from its old location in stats.xml was made
+                    # the way it works is:
+                    # after a files score is copied over from stats.xml - it will either have a real node in stats.xml or a
+                    # null placeholder one (see further down)
+                    # in the push command, null nodes get written to scores.txt as hash:NODATA
+                    # so the next time apply runs, we know that they had already been copied over, and we don't have to run
+                    # xmlstarlet again
+                    null_files["("$(echo $i | cut -sd : -f1)")"]+="$person"
+                else
+                    song_score=$(echo $i | rev | cut -sd : -f1 | rev)
+                    song_steps_type=$(echo $i | rev | cut -sd : -f2 | rev)
+                    song_difficulty=$(echo $i | rev | cut -sd : -f3 | rev)
+                    song_hash=$(echo $i | rev | cut -sd : -f4 | rev)
+                    scores["${highscorename}:${song_hash}:${song_difficulty}:${song_steps_type}"]+="${song_score}"
+                    all_scores+=("${song_hash}:${song_difficulty}:${song_steps_type}")
+                fi
             done
         done < <(find "${DIR}"/*.scores.txt -type f -print0)
     fi
 
-    readarray -t all_scores_unique < <(printf '%s\n' "${all_scores[@]}" | sort -u)
-    highest=""
-    for i in "${all_scores_unique[@]}"; do
-        for b in "${bros[@]}"; do
-            if [ -z "$highest" ]; then
-                highest="$b"
-            else
-                if [ -z "${scores[${b}:${i}]}" ]; then
-                    scores["${b}:${i}"]+="0.000000"
-                fi
+    # instead of using all_score_unique, use the all song hashes list
+    # that way when we come across something with no score we can build up a query for xmlstarlet
+    # for each hash
+    #   for each difficulty
+    #     for each step type
+    #       do the bros algorithm
+    # need a flag to decide if the hash has a score,
+    # if it doesn,t add it it to the list of hashes to attempt to copy a score from
+    declare -a orphans
+    for hash in "${!all_song_hashes[@]}"; do
+        had_score=false
+        for d in "${difficulties[@]}"; do
+            for t in "${step_types[@]}"; do
+                highest=""
+                for b in "${bros[@]}"; do
+                    if [ -z "$highest" ]; then
+                        highest="$b"
+                    else
+                        i="${hash}:${d}:${t}"
+                        if [ -z "${scores[${b}:${i}]}" ]; then
+                            scores["${b}:${i}"]+="0.000000"
+                        fi
 
-                if [ -z "${scores[${highest}:${i}]}" ]; then
-                    scores["${highest}:${i}"]+="0.000000"
-                fi
+                        if [ -z "${scores[${highest}:${i}]}" ]; then
+                            scores["${highest}:${i}"]+="0.000000"
+                        fi
 
-                if [ 1 -eq "$(echo "${scores[${b}:${i}]} > ${scores[${highest}:${i}]}" | bc)" ]; then
-                    highest="$b"
+                        if [ 1 -eq "$(echo "${scores[${b}:${i}]} > ${scores[${highest}:${i}]}" | bc)" ]; then
+                            highest="$b"
+                        fi
+                    fi
+                done
+                if ! [ "${scores[${highest}:${i}]}" = "0.000000" ]; then
+                    pranks["${i}"]+="${highest}:${scores[${highest}:${i}]}"
+                    had_score=true
                 fi
-            fi
+            done
         done
 
-        if ! [ "${scores[${highest}:${i}]}" = "0.000000" ]; then
-            pranks["${i}"]+="${highest}:${scores[${highest}:${i}]}"
+        # only try find files for which there is actually nothing in scores.txt
+        if [ "$had_score" = false ] && [ -z "${null_files[${hash}]}" ]; then
+            song="${hash_map[${hash}]}"
+            xmlstarlet_orphans_cmd="$xmlstarlet_orphans_cmd -i \"//SongScores//Song[@Dir=\\\"Songs/${song}/\\\"]//Steps//HighScore[1]//PercentDP\" \
+                                 -o \"SONG:${hash}\" -n \
+                                 -o \"STEPTYPES\" -n \
+                                 -v \"//SongScores//Song[@Dir=\\\"Songs/${song}/\\\"]//Steps//HighScore//PercentDP/ancestor::Steps/@StepsType\" -n \
+                                 -o \"DIFFICULTIES\" -n \
+                                 -v \"//SongScores//Song[@Dir=\\\"Songs/${song}/\\\"]//Steps//HighScore//PercentDP/ancestor::Steps/@Difficulty\" -n \
+                                 -o \"SCORES\" -n \
+                                 -v \"//SongScores//Song[@Dir=\\\"Songs/${song}/\\\"]//Steps//HighScore[1]//PercentDP\" -n \
+                                 --else \
+                                 -o \"SONG:${hash}\" -n \
+                                 -o \"NOSCORES\" -n \
+                                 --break"
+            orphans+=("$hash")
+        fi
+    done
+
+    scores_living_elsewhere=$(eval "xmlstarlet sel -t $xmlstarlet_orphans_cmd -o \"SONG\" \"${path_to_stats}\"")
+    IFS=$'\n' read -rd '' -a scores_elsewhere <<<"$scores_living_elsewhere"
+
+    empty_nodes=""
+    declare -a jjj_types
+    declare -a jjj_diffs
+    declare -a jjj_scores
+    past_steptypes=false
+    past_difficulties=false
+    past_scores=false
+    no_dump=false
+    for i in "${scores_elsewhere[@]}"; do
+        if [[ ${i:0:4} = "SONG" ]]; then
+            if [ "$no_dump" = false ]; then
+                for j in "${!jjj_types[@]}"; do
+                    pranks["${jjj_song}:${jjj_diffs[$j]}:${jjj_types[$j]}"]+="${me_highscore_name}:${jjj_scores[$j]}"
+                done
+            fi
+
+            jjj_types=()
+            jjj_diffs=()
+            jjj_scores=()
+            past_steptypes=false
+            past_difficulties=false
+            past_scores=false
+            no_dump=false
+            jjj_song=$(echo $i | cut -sd ':' -f2)
+            continue
+        fi
+
+        if [ "$i" = "NOSCORES" ]; then
+            faver="${all_song_hashes[${jjj_song}]}"
+
+            if [ "$faver" = "$me" ]; then
+                persons_folder="$faves_folder"
+            else
+                formatted_person="$(tr '[:lower:]' '[:upper:]' <<< ${faver:0:1})${faver:1}"
+                persons_folder=${external_favourite_naming_scheme/\%person\%/$formatted_person}
+            fi
+
+            path="$(echo ${hash_map[${jjj_song}]} | cut -sd / -f2)"
+            path=$(echo $path | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/g')
+            song_path="Songs/${persons_folder}/${path}/"
+            empty_nodes="${empty_nodes}\n<Song Dir=\"$song_path\" >\
+             \n<Steps Difficulty=\"Challenge\" StepsType=\"dance-single\">\
+             \n<HighScoreList>\
+             \n<LastPlayed>1933-01-02</LastPlayed>\
+             \n<NumTimesPlayed>1</NumTimesPlayed>\
+             \n</HighScoreList>\
+             \n</Steps>\
+             \n</Song>"
+            no_dump=true
+            continue
+        fi
+
+        if [ "$i" = "STEPTYPES" ]; then
+            past_steptypes=true
+            continue
+        fi
+
+        if [ "$i" = "DIFFICULTIES" ]; then
+            past_difficulties=true
+            continue
+        fi
+
+        if [ "$i" = "SCORES" ]; then
+            past_scores=true
+            continue
+        fi
+
+        if [ "$past_scores" = true ]; then
+            jjj_scores+=("$i")
+            continue
+        fi
+
+        if [ "$past_difficulties" = true ]; then
+            jjj_diffs+=("$i");
+            continue
+        fi
+
+        if [ "$past_steptypes" = true ]; then
+            jjj_types+=("$i")
+            continue
         fi
     done
 
@@ -141,11 +479,13 @@ if [ "$1" = "apply" ]; then
     new_nodes=""
     for j in "${!pranks[@]}"; do
         if [ -z "${processed_pranks[$j]}" ]; then
+            echo "$j :: ${pranks[$j]}"
             song_pranker=$(echo "${pranks[$j]}" | cut -sd : -f1)
             song_score=$(echo "${pranks[$j]}" | cut -sd : -f2)
-            song_steps_type=$(echo $j | rev | cut -sd : -f1 | rev)
-            song_difficulty=$(echo $j | rev | cut -sd : -f2 | rev)
-            song_faver=$(echo $j | rev | cut -sd : -f3 | rev | cut -sd / -f1)
+            song_steps_type=$(echo $j | cut -sd : -f3)
+            song_difficulty=$(echo $j | cut -sd : -f2)
+            song_faver=${all_song_hashes[$(echo $j | cut -sd : -f1)]}
+            song_hash=$(echo $j | cut -sd : -f1)
             song_tier=1
             for tier in "${tiers[@]}"; do
                 if [ 1 -eq "$(echo "$song_score < ${tier}" | bc)" ]; then
@@ -161,8 +501,9 @@ if [ "$1" = "apply" ]; then
                 persons_folder=${external_favourite_naming_scheme/\%person\%/$formatted_person}
             fi
 
-            song=$(echo $j | rev | cut -sd : -f3 | rev | cut -sd / -f2)
+            song=$(echo ${hash_map[$song_hash]} | cut -sd / -f2)
             song_dir="Songs/${persons_folder}/${song}/"
+            song_dir=$(echo $song_dir | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/g')
             new_nodes="$new_nodes\n<Song Dir=\"$song_dir\" >\
 \n<Steps Difficulty=\"$song_difficulty\" StepsType=\"$song_steps_type\" >\
 \n<HighScoreList>\
@@ -192,9 +533,9 @@ if [ "$1" = "apply" ]; then
                     # there are pranks for other difficulties of this chart
                     # we need to process them now because they need to live in the
                     # same song node
-                    if ! [ -z "${pranks[${song_faver}/${song}:${d}:${t}]}" ]; then
+                    if ! [ -z "${pranks[${song_hash}:${d}:${t}]}" ]; then
                         if ! [ "$d" = "$song_difficulty" ] || ! [ "$t" = "$song_steps_type" ]; then
-                            whatever="${pranks[${song_faver}/${song}:${d}:${t}]}"
+                            whatever="${pranks[${song_hash}:${d}:${t}]}"
                             ssong_pranker=$(echo $whatever | cut -sd : -f1)
                             ssong_score=$(echo $whatever | cut -sd : -f2)
                             ssong_tier=1
@@ -205,7 +546,7 @@ if [ "$1" = "apply" ]; then
                             done
                             ssong_tier=$(printf "Tier%02d" $ssong_tier)
 
-                            processed_pranks["${song_faver}/${song}:${d}:${t}"]+="pranked"
+                            processed_pranks["${song_hash}:${d}:${t}"]+="pranked"
                             new_nodes="$new_nodes\n<Steps Difficulty=\"${d}\" StepsType=\"${t}\" >\
 \n<HighScoreList>\
 \n<HighScore>\
@@ -240,11 +581,7 @@ if [ "$1" = "apply" ]; then
     $(eval "$cmd")
 
     last_tags_removed=$(head -n -2 "${path_to_stats}")
-    echo -e "${last_tags_removed}${new_nodes}\n</SongScores>\n</Stats>" > "${path_to_stats}"
-fi
-
-if [ "$1" = "pull" ]; then
-    scp groovenet@cameron1729.xyz:/mnt/media/GrooveNet/* "$DIR"
+    echo -e "${last_tags_removed}${new_nodes}\n${empty_nodes}\n</SongScores>\n</Stats>" > "${path_to_stats}"
 fi
 
 sed -i '1s/.*/<?xml version="1.0" encoding="UTF-8" ?>/' "${path_to_stats}"
diff --git a/SongSync.sh b/SongSync.sh
new file mode 100755 (executable)
index 0000000..a740a4d
--- /dev/null
@@ -0,0 +1,109 @@
+#!/bin/bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+source "$DIR/config.sh"
+
+if [ "$1" = "apply" ]; then
+    num_songs_files=$(find "${DIR}" -name "songs.*.txt" -printf "%f\n" | wc -l)
+
+    if [ "$num_songs_files" = "1" ]; then
+        echo "Everything up to date"
+        exit
+    fi
+
+    if [ "$num_songs_files" -gt "2" ]; then
+        echo "More than 2 songs files, something went wrong last time?"
+        exit
+    fi
+
+    new_songs_file=$(find "${DIR}" -name "songs.*.txt" -printf "%f\n" | sort -r | head -n1)
+    old_songs_file=$(find "${DIR}" -name "songs.*.txt" -printf "%f\n" | sort -r | tail -n1)
+
+    diff=$(diff -u "${DIR}/$old_songs_file" "${DIR}/$new_songs_file")
+
+    # path => hash
+    declare -A adds
+    # path => hash
+    declare -A removes
+    # path => path
+    declare -A moves
+
+    # algorithm
+    # remove everything with a -
+    #    move the folder to /tmp (rm it if it's already there) and name it using the hash
+    # download everything with a +
+    #    first check if it's in /tmp and copy it over from there
+    # determine which files need cache invalidation
+    #   they will be files with the same path appearing in adds and removes
+    #
+    # one downside of this approach is it will result in redownloading files already
+    # on disk instead of moving them
+    while read -r m; do
+        if [[ ${m:0:2} = "++" ]] || [[ ${m:0:2} = "--" ]] || [[ ${m:0:2} = "@@" ]]; then
+            continue
+        fi
+
+        echo "$m"
+        hash=$(echo $m | cut -sd ' ' -f1)
+        hash="${hash:1}"
+        path=$(echo $m | cut -sd / -f5,6)
+
+        if [[ ${m:0:1} = "+" ]]; then
+            if ! [ -z "${adds[${path}]}" ]; then
+                echo "More than one SM file in ${path}, aborting"
+                exit
+            fi
+            adds["${path}"]+="$hash"
+        fi
+
+        if [[ ${m:0:1} = "-" ]]; then
+            # It can happan that a path is already in removes if there are multiple SM files in a directory
+            # this should not really happen since there's the check above that refuses to add any files with that
+            # problem. However when I do the initial sync on our machines they will have problematic files.
+            #
+            # So just ignore it, the path is already in the removes list, so it will be nuked.
+            #
+            # I suppose it could happen that one of these borked paths were moved to a different location, but
+            # in that case there'd be multiple sms in the adds path. So... I guess it's OK.
+            if [ -z "${removes[${path}]}" ]; then
+               removes["${path}"]+="$hash"
+            fi
+        fi
+    done <<< "$diff"
+
+    for removal_path in "${!removes[@]}"; do
+        if [ -d "/tmp/${removes[$removal_path]}" ]; then
+            echo "need to rm /tmp/${removes[$removal_path]}"
+            rm "/tmp/${removes[$removal_path]}"
+        fi
+
+        echo "Moving ${removal_path} to /tmp/${removes[$removal_path]}"
+        mv "${path_to_songs}/${removal_path}" "/tmp/${removes[$removal_path]}"
+    done
+
+    for add_path in "${!adds[@]}"; do
+        pack_dir=$(echo $add_path | cut -sd / -f1)
+        if ! [ -z "${removes[${add_path}]}" ]; then
+            crc32="$(echo -n "${add_path}/" | gzip -c | tail -c8 | hexdump -n4 -e '"%u"')"
+            echo "CACHE INVALIDATION NEEDED FOR ${add_path} : ${crc32}"
+            rm "~/.openitg/Cache/Songs/${crc32}"
+        fi
+
+        # The file existed previously, just move that over.
+        if [ -d "/tmp/${adds[$add_path]}" ]; then
+            echo "Moving /tmp/${adds[$add_path]} to ${path_to_songs}/${add_path}"
+            mv "/tmp/${adds[$add_path]}" "${path_to_songs}/${add_path}"
+            continue
+        fi
+
+        echo "Creating dir for: ${add_path}"
+        mkdir -p "${path_to_songs}/${pack_dir}"
+        echo "Downloading files"
+        escaped_add_path="/mnt/media/Simfiles/"$(echo "$add_path" | sed 's/ /\\\\\\ /g' | sed "s/'/\\\\\\\\\\\'/g")
+        scp_command="scp -r groovenet@cameron1729.xyz:$escaped_add_path \"${path_to_songs}/${pack_dir}\""
+        echo "$scp_command"
+        eval "$scp_command"
+    done
+
+    rm ${old_songs_file}
+fi
diff --git a/pull.sh b/pull.sh
new file mode 100755 (executable)
index 0000000..108b751
--- /dev/null
+++ b/pull.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+source "$DIR/config.sh"
+
+date=$(date +%s)
+for f in ${DIR}/*.scores.txt; do
+    base=$(basename "$f" | cut -sd . -f1,2)
+    mv -- "$f" "${DIR}/${base}.${date}.txt"
+done
+
+for f in ${DIR}/*.favourites.txt; do
+    base=$(basename "$f" | cut -sd . -f1,2)
+    mv -- "$f" "${DIR}/${base}.${date}.txt"
+done
+
+scp groovenet@cameron1729.xyz:/mnt/media/GrooveNet/*.txt "$DIR"
+
+for f in ${DIR}/*.scores.txt; do
+    base=$(basename "$f" | cut -sd . -f1,2)
+    backed_up_file="${base}.${date}.txt"
+    if [ -f "${DIR}/$backed_up_file" ]; then
+        diff=$(diff "$f" "${DIR}/$backed_up_file")
+
+        if ! [ -z "$diff" ]; then
+            zip -rj "${DIR}/scores.zip" "${DIR}/$backed_up_file"
+        fi
+        rm "${DIR}/${backed_up_file}"
+    fi
+done
+
+for f in ${DIR}/*.favourites.txt; do
+    base=$(basename "$f" | cut -sd . -f1,2)
+    backed_up_file="${base}.${date}.txt"
+    if [ -f "${DIR}/$backed_up_file" ]; then
+        diff=$(diff "$f" "${DIR}/$backed_up_file")
+
+        if ! [ -z "$diff" ]; then
+            zip -rj "${DIR}/favourites.zip" "${DIR}/$backed_up_file"
+        fi
+        rm "${DIR}/${backed_up_file}"
+    fi
+done