269 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			269 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env bash
 | |
| # Copyright 2014 Vladimir Ivanov <ivvl82@gmail.com>
 | |
| # Distributed under the terms of the GNU General Public License v2
 | |
| 
 | |
| REPO_DIR=
 | |
| 
 | |
| # Specify the tournament here
 | |
| TOURNAMENT=
 | |
| 
 | |
| # Version information
 | |
| VERSION="0.3"
 | |
| 
 | |
| # Firefox User Agent
 | |
| FIREFOX_UA="Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0"
 | |
| 
 | |
| argv0=${0##*/}
 | |
| 
 | |
| function usage {
 | |
|     cat <<EOF
 | |
| Store chess games played on lichess.org
 | |
| 
 | |
| Usage:
 | |
|   $argv0 -t <num> [-u] [<url>]
 | |
|   $argv0 -h
 | |
|   $argv0 -v
 | |
| 
 | |
|   Put the script under the root directory of your repository or set
 | |
|   inner variable REPO_DIR to it. If the tournament is not the last one
 | |
|   (default), store its sub-directory in inner variable TOURNAMENT.
 | |
| 
 | |
|   The first form fills the results of chess games and stores its PGN
 | |
|   files, assuming that all the games were played by the same pair of
 | |
|   players on tour <num> of the tournament. The PGN files are sorted by
 | |
|   their timestamps, unless '-u' is set. Each game should be available
 | |
|   at the corresponding <url> (lichess.org).
 | |
| 
 | |
|   The second form shows this help output. The third form shows version
 | |
|   information.
 | |
| 
 | |
| Options:
 | |
|   -u            Don't sort games by their timestamps
 | |
| EOF
 | |
| 
 | |
|     exit "${1:-0}"
 | |
| }
 | |
| 
 | |
| function version {
 | |
|     exec echo "${argv0}-${VERSION}"
 | |
| }
 | |
| 
 | |
| function game_setup {
 | |
|     : ${REPO_DIR:=`dirname "$0"`}
 | |
| 
 | |
|     # If no tournament given, set it to the last one
 | |
|     if [[ -z $TOURNAMENT ]]; then
 | |
|         local year_dir=$(ls -1 -d ${REPO_DIR}/[0-9][0-9][0-9][0-9]/ | tail -1)
 | |
|         TOURNAMENT=$(ls -1 -d ${year_dir}[0-9]-*/ | tail -1 \
 | |
|             | sed -E "s|${REPO_DIR}/(.*)/|\1|")
 | |
|     fi
 | |
| 
 | |
|     # Configuration file for players
 | |
|     ply_ini=${REPO_DIR}/${TOURNAMENT}/players.ini
 | |
|     [[ -f $ply_ini ]] || die "File ${ply_ini} not found."
 | |
|     # Pairs of players on the tour
 | |
|     tour_info=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/tour_info
 | |
|     [[ -f $tour_info ]] || die "File ${tour_info} not found."
 | |
| }
 | |
| 
 | |
| function game_tmp_pgns {
 | |
|     TMP_PGN_FILES=
 | |
|     # Don't sort just one game
 | |
|     [[ $# == 1 ]] && SORT_GAMES=false
 | |
| 
 | |
|     for url in $@; do
 | |
|         local pgn_template="pgn"
 | |
|         [[ $url =~ ^(http://[^/]*)/([^/]*) ]]
 | |
|         # Link to annotated game PGN
 | |
|         local game_url=${BASH_REMATCH[1]}/game/export/${BASH_REMATCH[2]::8}.pgn
 | |
| 
 | |
|         # Append the timestamp of game for sorting
 | |
|         if $SORT_GAMES; then
 | |
|             local game_api=${BASH_REMATCH[1]}/api/game/${BASH_REMATCH[2]::8}
 | |
|             local api_response=$(curl -q --fail --location --silent "$game_api")
 | |
|             [[ -z $api_response ]] && die "Unreachable game API ${game_api}"
 | |
|             local game_time=$(sed -En "s/.*\"timestamp\":([0-9]+).*/\1/p" <<< "$api_response")
 | |
|             pgn_template+="-${game_time}"
 | |
|         fi
 | |
| 
 | |
|         # Store PGN file in a temporal location
 | |
|         local tmp_pgn=$(mktemp -t ${pgn_template}.XXXXXX)
 | |
|         TMP_PGN_FILES+=" $tmp_pgn"
 | |
|         trap "rm $TMP_PGN_FILES" EXIT
 | |
|         wget -q -U "$FIREFOX_UA" -O $tmp_pgn "$game_url" \
 | |
|             || die "Unreachable game PGN ${game_url}"
 | |
|     done
 | |
| 
 | |
|     $SORT_GAMES && TMP_PGN_FILES=$(xargs -n1 <<< "$TMP_PGN_FILES" | sort | xargs)
 | |
| }
 | |
| 
 | |
| function game_get_players {
 | |
|     # Make an associative array from Lichess nicks to players' names
 | |
|     declare -A NAMES=()
 | |
|     game_assoc_names
 | |
| 
 | |
|     local ply_names=
 | |
|     for pgn in $TMP_PGN_FILES; do
 | |
|         # Extract players on Lichess
 | |
|         local wt_lichess=$(sed -En "s/\[White \"([^\"]*)\"\]/\1/p" $pgn)
 | |
|         local bk_lichess=$(sed -En "s/\[Black \"([^\"]*)\"\]/\1/p" $pgn)
 | |
| 
 | |
|         game_add_player $wt_lichess
 | |
|         game_add_player $bk_lichess
 | |
|     done
 | |
| 
 | |
|     # Select the names of two players
 | |
|     players=( $(echo -en "$ply_names" | sort -u) )
 | |
|     [[ ${#players[@]} == 2 ]] || die "Players of the games are not the same."
 | |
| }
 | |
| 
 | |
| function game_assoc_names {
 | |
|     game_parse_config
 | |
|     local sections=$(grep -o "config_section_player[0-9]*" $tmp_ini)
 | |
|     for sect in $sections; do
 | |
|         eval $sect
 | |
|         NAMES+=( [$lichess]=$name )
 | |
|     done
 | |
| }
 | |
| 
 | |
| function game_parse_config {
 | |
|     # Temporary files
 | |
|     tmp_ini=$(mktemp -t `basename $ply_ini`.XXXXXX)
 | |
|     TMP_INI_FILES="${tmp_ini} ${tmp_ini}.prev"
 | |
|     trap "rm $TMP_INI_FILES $TMP_PGN_FILES" EXIT
 | |
| 
 | |
|     # Copy player INI file to the temporary location
 | |
|     # NOTE: an empty line is added to the file beginning in order to
 | |
|     # match the only first occurrence for non-GNU sed
 | |
|     echo > $tmp_ini
 | |
|     cat "$ply_ini" >> $tmp_ini
 | |
| 
 | |
|     # Remove tabs or spaces around the `='
 | |
|     sed -E -i.prev "s/[[:blank:]]*=[[:blank:]]*/=/" "$tmp_ini"
 | |
| 
 | |
|     # Transform section labels into function declaration
 | |
|     sed -E -i.prev "1,/^\[.*\]/s/^\[([^]]*)\]/config_section_\1() {/" "$tmp_ini"
 | |
|     sed -E -i.prev "s/^\[([^]]*)\]/}\\"$'\n'"config_section_\1() {/" "$tmp_ini"
 | |
|     echo -e "\n}" >> $tmp_ini
 | |
| 
 | |
|     # Source the file
 | |
|     source "$tmp_ini"
 | |
| }
 | |
| 
 | |
| function game_add_player {
 | |
|     local lichess_ply=$1
 | |
|     local ply=${NAMES[$lichess_ply]}
 | |
|     while [[ ! " ${NAMES[@]} " =~ \ $ply\  ]]; do
 | |
|         echo "The list of players:"
 | |
|         echo "${NAMES[@]}" | tr ' ' '\n' | sed "s/^/$(tput setaf 6)*$(tput sgr0) /"
 | |
|         echo -n "Type the name of ${lichess_ply}> "
 | |
|         read ply
 | |
|     done
 | |
|     ply_names+="${ply}\n"
 | |
| }
 | |
| 
 | |
| function game_add_to_repo {
 | |
|     local date_re="[0-9?]{2}\.[0-9?]{2}\.[0-9?]{4}"
 | |
|     local correct=false length_max=0
 | |
|     local white=${players[0]} black=${players[1]}
 | |
|     # Change field separator to read a file line by line
 | |
|     old_IFS=$IFS IFS=$'\n'
 | |
|     # Check if the tour number is correct
 | |
|     for line in $(cat "$tour_info"); do
 | |
|         if [[ $line =~ ^(${date_re}\ +([^\ ]+)\ +-\ +([^\ ]+)) ]]; then
 | |
|             local length=${#BASH_REMATCH[1]}
 | |
|             local fst_ply=${BASH_REMATCH[2]} snd_ply=${BASH_REMATCH[3]}
 | |
| 
 | |
|             if [[ $fst_ply == $white && $snd_ply == $black ]]; then
 | |
|                 local length_rec=${#BASH_REMATCH[1]}
 | |
|                 correct=true
 | |
|             fi
 | |
| 
 | |
|             if [[ $fst_ply == $black && $snd_ply == $white ]]; then
 | |
|                 local length_rec=${#BASH_REMATCH[1]}
 | |
| 
 | |
|                 local answer
 | |
|                 echo -n "Approve game with wrong players' sides? (Y/n)> "
 | |
|                 read answer
 | |
|                 [[ $answer =~ ^(Y|y|Yes|yes)$ ]] || exit 1
 | |
|                 white=$fst_ply black=$snd_ply correct=true
 | |
|             fi
 | |
| 
 | |
|             # Determine the maximal length of game records
 | |
|             (( $length > $length_max )) && length_max=$length
 | |
|         fi
 | |
|     done
 | |
|     IFS=$old_IFS
 | |
| 
 | |
|     if ! $correct; then
 | |
|         die "Game '${white} vs. ${black}' not found in ${tour_info}."
 | |
|     fi
 | |
| 
 | |
|     local pgn_date=$(sed -En "s/\[Date \"([^\"]*)\"\]/\1/p" "$tmp_pgn")
 | |
|     local pgn_date=$(tr "." "-" <<< "$pgn_date")
 | |
|     pgn_dir=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/${pgn_date}-${white}-vs-${black}
 | |
| 
 | |
|     [[ -d $pgn_dir ]] && die "Directory ${pgn_dir} already exist."
 | |
|     echo "Creating directory ${pgn_dir}..."
 | |
|     mkdir -p "$pgn_dir"
 | |
| 
 | |
|     echo "Storing PGN file..."
 | |
|     cp "$tmp_pgn" "${pgn_dir}/1.pgn"
 | |
| 
 | |
|     echo "Updating tour_info..."
 | |
|     local game_date=${pgn_date:8:2}.${pgn_date:5:2}.${pgn_date::4}
 | |
|     local result=$(sed -En "s/\[Result \"([^\"]*)\"\]/\1/p" $tmp_pgn)
 | |
|     local spaces=$((length_max - length_rec + 1))
 | |
|     local sep=$(printf "%${spaces}s" " ")
 | |
| 
 | |
|     # Change the representation of draw
 | |
|     [[ $result == 1/2-1/2 ]] && result=1/2
 | |
| 
 | |
|     sed -E -i.orig \
 | |
|         "s|${date_re}( +${white} +- +${black})|${game_date}\1${sep}${result}|" "$tour_info"
 | |
|     rm "${tour_info}.orig"
 | |
| }
 | |
| 
 | |
| function game_git_commit {
 | |
|     (
 | |
|         cd $REPO_DIR
 | |
|         git add "${pgn_dir}/1.pgn" "$tour_info"
 | |
|         git commit -m "Tour ${TOUR#0}: ${white} vs. ${black}."
 | |
|         git push
 | |
|     )
 | |
| }
 | |
| 
 | |
| function die {
 | |
|     echo "$@" >&2
 | |
|     exit 1
 | |
| }
 | |
| 
 | |
| function checkargs {
 | |
|     [[ $OPTARG =~ ^[0-9]+$ ]] || die "Incorrect tour number."
 | |
|     TOUR=$(printf "%02g" $OPTARG)
 | |
| }
 | |
| 
 | |
| SORT_GAMES=true
 | |
| while getopts t:uhv opt; do
 | |
|     case $opt in
 | |
|         t) checkargs        ;;
 | |
|         u) SORT_GAMES=false ;;
 | |
|         h) usage            ;;
 | |
|         v) version          ;;
 | |
|         *) usage 1          ;;
 | |
|     esac
 | |
| done
 | |
| 
 | |
| shift $(($OPTIND - 1))
 | |
| # For now, tour number should be given explicitly
 | |
| [[ -z $TOUR || $# == 0 ]] && usage 1
 | |
| 
 | |
| game_setup
 | |
| (cd $REPO_DIR; git pull)        # update the repository
 | |
| game_tmp_pgns $@
 | |
| game_get_players
 | |
| game_add_to_repo
 | |
| game_git_commit
 | |
| 
 | |
| exit 0
 |