├── .gitignore ├── LICENSE ├── README.md ├── download_days.sh ├── mvnw ├── mvnw.cmd ├── pom.xml └── src └── main └── java └── rr └── hikvisiondownloadassistant ├── DateConverter.java ├── DigestAuth.java ├── IsapiRestClient.java ├── Main.java ├── Model.java ├── Options.java └── OutputFormatter.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | /.mvn 3 | !**/src/main/** 4 | !**/src/test/** 5 | /src/main/resources/cert.p12 6 | .DS_Store 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ryan Richard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hikvision Download Assistant 2 | 3 | A command-line tool to make searching and downloading photos and videos from your Hikvision cameras easy... 4 | without requiring installation of any Hikvision software on your computer! 5 | 6 | ## Usage 7 | 8 | Hikvision Download Assistant is simple command line tool that connects to your Hikvision camera or NVR 9 | using the ISAPI web API to perform searches for videos and photos. It writes `curl` commands to the screen 10 | that you can then use to download each photo or video from the ISAPI API. 11 | 12 | Like most command-line tools, it is designed to be easily composed with other command-line tools. 13 | The list of search results goes to `stdout`. 14 | All other helpful header and footer text is sent to `stderr` so it can be easily excluded from pipes and redirects. 15 | 16 | Run using `java -jar hikvision-download-assistant.jar `. 17 | 18 | ### Usage statement 19 | 20 | ``` 21 | Usage: java -jar hikvision-download-assistant.jar [-hqV] [-d=] [-f=] [-o=] [-p=] [-t=] [-u=] HOST USERNAME PASSWORD 22 | HOST Connect to this host or IP address to perform search. 23 | USERNAME Use this username when connecting to perform search. 24 | PASSWORD Use this password when connecting to perform search. 25 | -d, --table-delimiter= 26 | The column delimiter for table output. Defaults to '|'. 27 | -f, --from-time= 28 | Search starting from this time, entered using English natural language. Defaults to '24 hours ago'. 29 | -h, --help Show this help message and exit. 30 | -o, --output= 31 | Output format. Can be 'table' or 'json'. Defaults to 'table'. 32 | -p, --output-password= 33 | Output a different password in the printed curl commands, e.g. '$PASSWORD'. 34 | -q, --quiet Suppress header and footer. 35 | -t, --to-time= Search up to this time, entered using English natural language. Defaults to 'now'. 36 | -u, --output-username= 37 | Output a different username in the printed curl commands, e.g. '$USERNAME'. 38 | -V, --version Print version information and exit. 39 | ``` 40 | 41 | ### Examples 42 | 43 | With default options: 44 | 45 | ```bash 46 | $ java -jar hikvision-download-assistant.jar 192.168.1.64 admin passsword123 47 | Getting photos and videos from "Saturday May 30, 2020 at 7:43:52 PM PDT" to "Sunday May 31, 2020 at 7:43:53 PM PDT" 48 | 49 | Type|EventType|Start|End|Curl 50 | ----------------------------- 51 | VIDEO|ALLEVENT|2020-05-31T19:01:06-0700|2020-05-31T19:01:22-0700|curl -f --anyauth --user admin:password123 -X GET -d 'rtsp://192.168.1.64/Streaming/tracks/101/?starttime=20200601T020106Z&endtime=20200601T020122Z&name=ch01_00000000008001213&size=6836476' 'http://192.168.1.64/ISAPI/ContentMgmt/download' --output 2020-05-31T19-01-06.mp4 52 | PHOTO|MOTION|2020-05-31T19:01:09-0700|2020-05-31T19:01:09-0700|curl -f --anyauth --user admin:password123 'http://192.168.1.64/ISAPI/Streaming/tracks/103/?starttime=20200601T020109Z&endtime=20200601T020109Z&name=ch01_00000000005030201&size=574906' --output 2020-05-31T19-01-09.jpeg 53 | PHOTO|MOTION|2020-05-31T19:01:10-0700|2020-05-31T19:01:10-0700|curl -f --anyauth --user admin:password123 'http://192.168.1.64/ISAPI/Streaming/tracks/103/?starttime=20200601T020110Z&endtime=20200601T020110Z&name=ch01_00000000005030301&size=710770' --output 2020-05-31T19-01-10.jpeg 54 | VIDEO|ALLEVENT|2020-05-31T19:35:29-0700|2020-05-31T19:35:47-0700|curl -f --anyauth --user admin:password123 -X GET -d 'rtsp://192.168.1.64/Streaming/tracks/101/?starttime=20200601T023529Z&endtime=20200601T023547Z&name=ch01_00000000008001313&size=2933900' 'http://192.168.1.64/ISAPI/ContentMgmt/download' --output 2020-05-31T19-35-29.mp4 55 | PHOTO|MOTION|2020-05-31T19:35:34-0700|2020-05-31T19:35:34-0700|curl -f --anyauth --user admin:password123 'http://192.168.1.64/ISAPI/Streaming/tracks/103/?starttime=20200601T023534Z&endtime=20200601T023534Z&name=ch01_00000000005031401&size=537158' --output 2020-05-31T19-35-34.jpeg 56 | 57 | Found 2 videos and 4 photos 58 | ``` 59 | 60 | With `--output json`: 61 | 62 | ```bash 63 | $ java -jar hikvision-download-assistant.jar 192.168.1.64 admin passsword123 --output json 64 | Getting photos and videos from "Friday Jun 5, 2020 at 12:14:53 PM PDT" to "Saturday Jun 6, 2020 at 12:14:54 PM PDT" 65 | 66 | { 67 | "metadata" : { 68 | "host" : "192.168.1.64", 69 | "fromHumanReadableTime" : "Friday Jun 5, 2020 at 12:14:57 PM PDT", 70 | "toHumanReadableTime" : "Saturday Jun 6, 2020 at 12:14:57 PM PDT", 71 | "fromTime" : "2020-06-05T12-14-57", 72 | "toTime" : "2020-06-06T12-14-57" 73 | }, 74 | "results" : [ { 75 | "mediaType" : "VIDEO", 76 | "startTime" : 1591456673000, 77 | "endTime" : 1591456710000, 78 | "eventType" : "ALLEVENT", 79 | "curlCommand" : "curl -f --anyauth --user admin:password123 -X GET -d 'rtsp://192.168.1.64/Streaming/tracks/101/?starttime=20200606T151753Z&endtime=20200606T151830Z&name=ch01_00000000021000213&size=4938980' 'http://192.168.1.64/ISAPI/ContentMgmt/download' --output 2020-06-06T08-17-53.mp4" 80 | }, { 81 | "mediaType" : "PHOTO", 82 | "startTime" : 1591456677000, 83 | "endTime" : 1591456677000, 84 | "eventType" : "MOTION", 85 | "curlCommand" : "curl -f --anyauth --user admin:password123 'http://192.168.1.64/ISAPI/Streaming/tracks/103/?starttime=20200606T151757Z&endtime=20200606T151757Z&name=ch01_00000000020004001&size=469186' --output 2020-06-06T08-17-57.jpeg" 86 | }, { 87 | "mediaType" : "VIDEO", 88 | "startTime" : 1591456962000, 89 | "endTime" : 1591456977000, 90 | "eventType" : "ALLEVENT", 91 | "curlCommand" : "curl -f --anyauth --user admin:password123 -X GET -d 'rtsp://192.168.1.64/Streaming/tracks/101/?starttime=20200606T152242Z&endtime=20200606T152257Z&name=ch01_00000000021000713&size=1814988' 'http://192.168.1.64/ISAPI/ContentMgmt/download' --output 2020-06-06T08-22-42.mp4" 92 | }, { 93 | "mediaType" : "PHOTO", 94 | "startTime" : 1591456967000, 95 | "endTime" : 1591456967000, 96 | "eventType" : "MOTION", 97 | "curlCommand" : "curl -f --anyauth --user admin:password123 'http://192.168.1.64/ISAPI/Streaming/tracks/103/?starttime=20200606T152247Z&endtime=20200606T152247Z&name=ch01_00000000020013201&size=404225' --output 2020-06-06T08-22-47.jpeg" 98 | } ] 99 | } 100 | 101 | Found 2 videos and 2 photos 102 | ``` 103 | 104 | ### Unix/MacOS pipeline examples 105 | 106 | Filtering and choosing a column using `jq`: 107 | 108 | ```bash 109 | java -jar hikvision-download-assistant.jar 192.168.1.64 admin $PASSWORD --quiet --output json | jq '.results[] | select(.eventType=="MOTION") | .startTime' 110 | ``` 111 | 112 | Filtering and choosing a column using `grep` and `cut`: 113 | 114 | ```bash 115 | java -jar hikvision-download-assistant.jar 192.168.1.64 admin $PASSWORD --quiet | grep MOTION | cut -d '|' -f 3 116 | ``` 117 | 118 | Executing all of the returned curl commands to download all of the results to the current 119 | working directory (assuming `bash` is your shell): 120 | 121 | ```bash 122 | java -jar hikvision-download-assistant.jar 192.168.1.64 admin $PASSWORD --quiet | cut -d '|' -f 5 | while read curl_cmd; do eval $curl_cmd; done 123 | ``` 124 | 125 | ### Using `--from-time` and `--to-time` 126 | 127 | These options take English natural language and try their best to understand what you mean. 128 | 129 | Examples: 130 | 131 | - `now` 132 | - `10 pm yesterday` 133 | - `noon yesterday` 134 | - `5 pm` - 5 pm today 135 | - `6:50 today` - caution, this is 6:50 AM, no matter what current time 136 | - `6:50 am today` or `6:50 pm today` - safer alternative so you don't get AM by accident 137 | - `last thursday` - at the current time of day, but different date 138 | - `1 week ago` - at the current time of day, but different date 139 | - `oct 3rd` - at the current time of day, but different date 140 | - `2/14/20 at 2 am` 141 | - ...[and many more](http://natty.joestelmach.com/doc.jsp) 142 | 143 | It will not always guess correctly, so the first line of output will always print what it guessed so you can confirm. 144 | 145 | Your `--to-time` can be a date/time in the future. This may be helpful if your camera's system time is wrong. 146 | 147 | ### The `download_days.sh` script 148 | 149 | The [`download_days.sh`](download_days.sh) script is an example of using this app in a shell script. 150 | It downloads all of the photos and videos for the requested number of days and then 151 | generates a simple web-based UI for browsing all of the downloaded photos and videos. 152 | 153 | ### Other useful command-line tips for manipulating the downloaded photos and videos 154 | 155 | Make a playlist out of all the videos in the current directory, and then open it with VLC: 156 | 157 | ```bash 158 | find $(pwd) -maxdepth 1 -name '*.mp4' | sort > playlist.m3u 159 | vlc --play-and-exit --no-video-title-show playlist.m3u 160 | ``` 161 | 162 | Convert all the video files in the current directory into a format that is playable by web browsers, 163 | assuming that you have `ffmpeg` installed: 164 | 165 | ```bash 166 | for file in $(find $(pwd) -maxdepth 1 -name '*.mp4'); do ffmpeg -err_detect ignore_err -i "$file" -c copy $(dirname "$file")/$(basename "$file" .mp4).fixed.mp4; done 167 | ``` 168 | 169 | ## Why would I need this? 170 | 171 | Why, you ask? Several reasons! 172 | 173 | - The Hikvision in-browser web UI software for browsing and downloading videos and photos is *no 174 | longer compatible with MacOS at all*. When you open their web UI, the "Live View" 175 | and "Configuration" tabs will appear, but the "Picture" and "Playback" tabs will not appear. 176 | - The Hikvision in-browser web UI software for browsing and downloading videos and photos for 177 | MS Windows works but still requires installing Hikvision software on your computer. 178 | Some people may not want to install this software. 179 | - Hikvision offers a free application called 180 | [iVMS-4200](https://www.hikvision.com/en/products/software/ivms-4200/) 181 | which does offer these capabilities, which does work on both MacOS and Windows. 182 | However, some people might wish to avoid installing it. 183 | Some users have complained in online forums about the design and usability of the application. 184 | Also, installing this software on a Mac requires entering your admin password several times, 185 | indicating that it is making some kind of changes to your OS, and it's not clear how to uninstall 186 | all of its side effects. 187 | - There doesn't seem to be a linux version of iVMS-4200, so there is no obvious way to download 188 | photos and videos on a linux computer. 189 | - Both the Hikvision web UI and the iVMS-4200 only allow you to download one 190 | page of video or photo search results at a time, so it is not clear how to easily download 191 | large numbers of photos and videos. 192 | - You can still configure your camera using the Hikvision in-browser web UI software, so if 193 | there were another tool to help you download videos and photos, then you wouldn't need to 194 | install any software from Hikvision. Well, now there is! 195 | 196 | ## Prerequisites 197 | 198 | ### Prerequisites on your computer 199 | 200 | This application is written in Java, and therefore can run on pretty much any platform. 201 | 202 | You'll need a Java Runtime Environment on your computer. It must be Java version 11 or higher. 203 | 204 | Good news, you might already have one! Open your command terminal and type `java -version` to check. 205 | 206 | Don't have one? You could install any JRE that you like, for example you could 207 | choose to install [OpenJDK](https://openjdk.java.net/install/index.html), 208 | e.g. `brew install openjdk` on a Mac. 209 | 210 | ### Prerequisites on your Hikvision device(s) 211 | 212 | There are no prerequisites on your camera. Here are some helpful tips for configuring your camera. 213 | 214 | Note that this app uses digest authentication, which is the default setting on Hikvision cameras, so you do *not* need 215 | to enable basic authentication to use this app. You also do *not* need to enable Hikvision-CGI to use this app. 216 | 217 | You'll want to make sure that your camera's system clock is correct. You can check this in the camera's web UI 218 | under System -> System Settings -> Time Settings. You'll probably also want to set the correct time zone on that 219 | same web page. 220 | 221 | You may wish to enable the Daylight Savings Time feature to avoid having your computer's 222 | time and your camera's time differ by one hour in the summer. This can be enabled in the camera's web UI 223 | under System -> System Settings -> DST. Be sure to also change the DST start time, end time, and bias 224 | to match your local DST schedule. 225 | For the US you would check the `Enable DST box`, start on `Mar Second Sun 02`, end on `Nov First Sun 02`, 226 | and set a bias of `60minutes`. Don't forget to click "Save". 227 | 228 | If you are formatting an sdcard in the Hikvision camera's web UI, you can choose the 229 | photo to video ratio for setting quotas on the same web page. These settings only apply during formatting, so be 230 | sure to set these to your preferred values before you format the sdcard. 231 | 232 | You may wish to set up [motion-activated recording](https://www.vueville.com/home-security/cctv/ip-cameras/hikvision-motion-detection-setup). 233 | 234 | You may also wish to set up [event-triggered photos](http://hikvision.com/UploadFile/File/2014331155115857.pdf) 235 | to automatically take a series of photos for every motion event, in addition to the video. 236 | 237 | ### Related tools (not required) 238 | 239 | - You'll probably want [`curl`](https://ec.haxx.se), but you already have that. 240 | - You might like to install [VLC](https://www.videolan.org/vlc/) to view 241 | the downloaded videos, e.g. `brew cask install vlc` on a Mac. 242 | - You might like [`ffmpeg`](https://ffmpeg.org) to convert the downloaded videos, 243 | e.g. `brew install ffmpeg` on a Mac. 244 | 245 | ## Compatible cameras and DVR/NVRs 246 | 247 | This was developed and tested using my Hikvision model DS-2CD2185FWD-I IP camera 248 | using Firmware version `V5.6.3 build 190923`. 249 | 250 | Theoretically, it should work with any Hikvision camera. 251 | 252 | I believe that the Hikvision NVRs offer the same ISAPI endpoint (`POST /ISAPI/ContentMgmt/search`) so 253 | it should theoretically work with those too. 254 | 255 | You mileage may vary. Github issues and PRs are welcome. 256 | 257 | ## Installing hikvision-download-assistant 258 | 259 | Download the latest release jar file from 260 | https://github.com/cfryanr/hikvision-download-assistant/releases/latest 261 | in your browser. 262 | 263 | Or download with `curl`, assuming you're using `bash` as your shell and that want to keep the file in `/usr/local/bin`: 264 | 265 | ```bash 266 | cd /usr/local/bin && { curl -fLO https://github.com/cfryanr/hikvision-download-assistant/releases/download/v1.1.0/hikvision-download-assistant.jar; cd -; } 267 | ``` 268 | 269 | If you would like to use `download_days.sh`, then download it to the same directory as the jar file: 270 | 271 | ```bash 272 | cd /usr/local/bin && { curl -fLO https://raw.githubusercontent.com/cfryanr/hikvision-download-assistant/v1.1.0/download_days.sh; chmod 755 download_days.sh; cd -; } 273 | ``` 274 | 275 | ## Building 276 | 277 | If you would prefer to compile the source code yourself, you'll need to install a Java JDK, 278 | e.g. `brew install openjdk` on a Mac. 279 | 280 | To build, run `./mvnw clean package` from the top-level directory of the project. 281 | 282 | The compiler will output a file called `target/hikvision-download-assistant-1.0-SNAPSHOT-jar-with-dependencies.jar`. 283 | If you'd like, you can copy this to whatever directory and filename you like. 284 | 285 | Run it with `java -jar target/hikvision-download-assistant-1.0-SNAPSHOT-jar-with-dependencies.jar ` 286 | 287 | ## Copyright and Licence 288 | 289 | `Copyright (c) 2020 Ryan Richard` 290 | 291 | Licensed under MIT. See [LICENSE](LICENSE) file for license. 292 | 293 | The author of this software is not affiliated with Hikvision Digital Technology Co., the maker of Hikvision cameras, 294 | and this software is not endorsed by Hikvision Digital Technology Co. 295 | -------------------------------------------------------------------------------- /download_days.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Copyright (c) 2020 Ryan Richard 5 | # 6 | # An example of using hikvision-download-assistant to download 7 | # several days worth of videos and photos, organizing them into 8 | # directories per day, and presenting a simple UI for browsing 9 | # and viewing them. 10 | # 11 | # Note: Requires ffmpeg and jq 12 | # 13 | # Usage: HIKVISION_PASSWORD=mypass download_days 14 | # When the argument is 0, downloads only today's photos and videos up to now. 15 | # When the argument is N, downloads today's photos and videos up to now plus the previous N days. 16 | # 17 | # Assumes that you have installed this script into the same directory as the hikvision-download-assisant.jar file. 18 | # 19 | # This script is safe to run multiple times for the same output directory. It will retain your previous files 20 | # and will only download photos and videos that were not previously downloaded. 21 | # 22 | 23 | set -eo pipefail 24 | 25 | # The user should set these environment variables or else it will use these defaults 26 | : "${HIKVISION_USERNAME:=admin}" 27 | : "${HIKVISION_HOST:=192.168.1.64}" 28 | 29 | # Get the command-line arguments 30 | NUM_DAYS=$1 31 | DOWNLOAD_DIR=$2 32 | 33 | if [[ -z "$HIKVISION_PASSWORD" ]]; then 34 | echo "ERROR: Please use \$HIKVISION_PASSWORD to set your password." >&2 35 | exit 1 36 | fi 37 | 38 | if [[ -z "$NUM_DAYS" ]] || ! [[ $NUM_DAYS =~ ^[0-9]+$ ]]; then 39 | echo "ERROR: Please use number of days to download as the first argument." >&2 40 | exit 1 41 | fi 42 | 43 | if [[ -z "$DOWNLOAD_DIR" ]]; then 44 | echo "ERROR: Please specify download destination directory as the second argument." >&2 45 | exit 1 46 | fi 47 | 48 | set -u 49 | 50 | IFS=$'\n' 51 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 52 | INDEX_FILENAME="index.html" 53 | 54 | mkdir -p "$DOWNLOAD_DIR" 55 | 56 | for DAYS_AGO in $(seq 0 "$NUM_DAYS"); do 57 | 58 | if [[ $DAYS_AGO -eq 0 ]]; then 59 | from="today at 12:00:00 AM" 60 | to="now" 61 | else 62 | from="$DAYS_AGO days ago at 12:00:00 AM" 63 | to="$DAYS_AGO days ago at 11:59:59 PM" 64 | fi 65 | 66 | SEARCH_RESULT=$(java -jar "$SCRIPT_DIR/hikvision-download-assistant.jar" \ 67 | "$HIKVISION_HOST" "$HIKVISION_USERNAME" "$HIKVISION_PASSWORD" \ 68 | --from-time "$from" --to-time "$to" \ 69 | --output json --quiet) 70 | 71 | RESULT_DATE=$(echo "$SEARCH_RESULT" | jq -r .metadata.fromTime | cut -c1-10) 72 | 73 | DAY_DIR="$DOWNLOAD_DIR/$RESULT_DATE" 74 | mkdir -p "$DAY_DIR" 75 | pushd "$DAY_DIR" >/dev/null 76 | 77 | for RESULT in $(echo "$SEARCH_RESULT" | jq -r --compact-output '.results[]'); do 78 | 79 | CURL_COMMAND=$(echo "$RESULT" | jq -r '.curlCommand') 80 | DOWNLOAD_FILENAME=$(echo "$CURL_COMMAND" | rev | cut -d ' ' -f 1 | rev) 81 | 82 | if [[ $DOWNLOAD_FILENAME == *mp4 ]]; then 83 | 84 | # For videos, download and transcode 85 | FIXED_FILENAME="$(dirname "$DOWNLOAD_FILENAME")/$(basename "$DOWNLOAD_FILENAME" .mp4).fixed.mp4" 86 | 87 | if ! [[ -f $FIXED_FILENAME ]]; then 88 | echo "Downloading $DOWNLOAD_FILENAME" 89 | eval "$CURL_COMMAND -s" 90 | echo "Transcoding $DOWNLOAD_FILENAME" 91 | ffmpeg -err_detect ignore_err -i "$DOWNLOAD_FILENAME" -c copy "$FIXED_FILENAME" -hide_banner -loglevel warning 92 | rm "$DOWNLOAD_FILENAME" 93 | else 94 | echo "Already downloaded $DOWNLOAD_FILENAME" 95 | fi 96 | 97 | else 98 | 99 | # For photos, just download 100 | if ! [[ -f $DOWNLOAD_FILENAME ]]; then 101 | echo "Downloading $DOWNLOAD_FILENAME" 102 | eval "$CURL_COMMAND -s" 103 | else 104 | echo "Already downloaded $DOWNLOAD_FILENAME" 105 | fi 106 | fi 107 | 108 | done # done downloading all files in the day directory 109 | 110 | echo "Making $INDEX_FILENAME for directory $DAY_DIR" 111 | echo 'Home' >"$INDEX_FILENAME" 112 | echo "

Downloaded photos and videos for $RESULT_DATE

" >>"$INDEX_FILENAME" 113 | FILE_ARRAY=($(find "$(pwd)" -maxdepth 1 \( -name "*.fixed.mp4" -o -name "*.jpeg" \) | sort)) 114 | FILE_COUNT=${#FILE_ARRAY[@]} 115 | for ((i = 0; i < FILE_COUNT; i++)); do 116 | CURRENT_FILE=${FILE_ARRAY[$i]} 117 | CURRENT_FILE_BASENAME=$(basename "$CURRENT_FILE") 118 | CURRENT_FILE_HTML="${CURRENT_FILE_BASENAME}.html" 119 | 120 | PREVIOUS_FILE="" 121 | if ((i > 0)); then 122 | PREVIOUS_FILE=${FILE_ARRAY[$i - 1]} 123 | PREVIOUS_FILE_BASENAME=$(basename "$PREVIOUS_FILE") 124 | fi 125 | 126 | NEXT_FILE="" 127 | if ((i < FILE_COUNT - 1)); then 128 | NEXT_FILE=${FILE_ARRAY[$i + 1]} 129 | NEXT_FILE_BASENAME=$(basename "$NEXT_FILE") 130 | fi 131 | 132 | echo 'Back to day' >"$CURRENT_FILE_HTML" 133 | if [[ -n "$PREVIOUS_FILE" ]]; then 134 | echo ' | Previous' >>"$CURRENT_FILE_HTML" 135 | else 136 | echo ' | Previous' >>"$CURRENT_FILE_HTML" 137 | fi 138 | if [[ -n "$NEXT_FILE" ]]; then 139 | echo ' | Next' >>"$CURRENT_FILE_HTML" 140 | else 141 | echo ' | Next' >>"$CURRENT_FILE_HTML" 142 | fi 143 | echo '

'"$CURRENT_FILE_BASENAME"'

' >>"$CURRENT_FILE_HTML" 144 | 145 | if [[ "$CURRENT_FILE" == *mp4 ]]; then 146 | echo '' >>"$CURRENT_FILE_HTML" 147 | echo '' >>"$INDEX_FILENAME" 148 | else 149 | echo '' >>"$CURRENT_FILE_HTML" 150 | echo '' >>"$INDEX_FILENAME" 151 | fi 152 | done 153 | 154 | if [[ FILE_COUNT -eq 0 ]]; then 155 | echo "

No downloaded photos or videos for $RESULT_DATE

" >"$INDEX_FILENAME" 156 | fi 157 | 158 | popd >/dev/null 159 | 160 | done 161 | 162 | echo "Making $INDEX_FILENAME for directory $DOWNLOAD_DIR" 163 | pushd "$DOWNLOAD_DIR" >/dev/null 164 | echo '

Downloaded photos and videos by day

' >"$INDEX_FILENAME" 165 | echo '

Powered by hikvision-download-assistant

    ' >>"$INDEX_FILENAME" 166 | for DATE_DIR in $(find ./* -maxdepth 1 -type d | sort); do 167 | DATE_DIR=$(basename "$DATE_DIR") 168 | echo '
  • '"$(basename "$DATE_DIR")"'
  • ' >>"$INDEX_FILENAME" 169 | done 170 | echo '
' >>"$INDEX_FILENAME" 171 | 172 | echo "Done. Wrote top-level index file:$(pwd)/$INDEX_FILENAME" 173 | 174 | if [[ $(uname) == "Darwin" ]]; then 175 | open $INDEX_FILENAME 176 | fi 177 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | rr 8 | hikvision-download-assistant 9 | 1.0-SNAPSHOT 10 | hikvision-download-assistant 11 | Helping you download photos and videos from Hikvision's IP camera API 12 | 13 | jar 14 | 15 | 16 | UTF-8 17 | 11 18 | 19 | 20 | 21 | 22 | com.fasterxml.jackson.dataformat 23 | jackson-dataformat-xml 24 | 2.11.0 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 1.18.12 31 | provided 32 | 33 | 34 | 35 | com.joestelmach 36 | natty 37 | 0.13 38 | 39 | 40 | 41 | 42 | org.slf4j 43 | slf4j-jdk14 44 | 1.7.30 45 | 46 | 47 | 48 | info.picocli 49 | picocli 50 | 4.3.2 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-compiler-plugin 59 | 3.8.1 60 | 61 | 11 62 | 11 63 | 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-assembly-plugin 69 | 3.3.0 70 | 71 | 72 | package 73 | 74 | single 75 | 76 | 77 | 78 | 79 | 80 | rr.hikvisiondownloadassistant.Main 81 | 82 | 83 | 84 | 85 | jar-with-dependencies 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/java/rr/hikvisiondownloadassistant/DateConverter.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Ryan Richard 2 | 3 | package rr.hikvisiondownloadassistant; 4 | 5 | import java.text.ParseException; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Date; 8 | import java.util.TimeZone; 9 | 10 | public class DateConverter { 11 | 12 | private static final SimpleDateFormat apiDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 13 | private static final SimpleDateFormat localDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 14 | private static final SimpleDateFormat localFilenameDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss"); 15 | private static final SimpleDateFormat localHumanDateFormat = new SimpleDateFormat("EEEE MMM d, yyyy 'at' h:mm:ss aaa z"); 16 | 17 | static { 18 | apiDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 19 | } 20 | 21 | public static Date apiStringToDate(String timeString) { 22 | try { 23 | return apiDateFormat.parse(timeString); 24 | } catch (ParseException e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | 29 | public static String dateToApiString(Date time) { 30 | return apiDateFormat.format(time); 31 | } 32 | 33 | public static String dateToLocalString(Date time) { 34 | return localDateFormat.format(time); 35 | } 36 | 37 | public static String dateToLocalFilenameString(Date time) { 38 | return localFilenameDateFormat.format(time); 39 | } 40 | 41 | public static String dateToLocalHumanString(Date time) { 42 | return localHumanDateFormat.format(time); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/rr/hikvisiondownloadassistant/DigestAuth.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Ryan Richard 2 | 3 | package rr.hikvisiondownloadassistant; 4 | 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import java.net.http.HttpHeaders; 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.security.SecureRandom; 11 | import java.util.Arrays; 12 | import java.util.Base64; 13 | import java.util.Map; 14 | import java.util.Optional; 15 | import java.util.stream.Collectors; 16 | 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | import static javax.xml.bind.DatatypeConverter.printHexBinary; 19 | 20 | @RequiredArgsConstructor 21 | public class DigestAuth { 22 | 23 | private static MessageDigest messageDigest; 24 | private static SecureRandom random; 25 | 26 | private final HttpHeaders unauthorizedResponseHeaders; 27 | private final String requestMethod; 28 | private final String requestPath; 29 | private final String username; 30 | private final String password; 31 | 32 | static { 33 | try { 34 | messageDigest = MessageDigest.getInstance("MD5"); 35 | random = SecureRandom.getInstance("SHA1PRNG"); 36 | random.setSeed(System.currentTimeMillis()); 37 | } catch (NoSuchAlgorithmException e) { 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | 42 | public String getAuthorizationHeaderValue() { 43 | Optional wwwAuthenticateHeader = unauthorizedResponseHeaders.firstValue("www-authenticate"); 44 | if (wwwAuthenticateHeader.isEmpty()) { 45 | throw new RuntimeException("Expected digest auth challenge but did not find it"); 46 | } 47 | String authChallenge = wwwAuthenticateHeader.get(); 48 | if (!authChallenge.startsWith("Digest ")) { 49 | throw new RuntimeException("Expected digest auth challenge but did not find it"); 50 | } 51 | 52 | String[] authenticateFields = authChallenge.substring("Digest ".length()).replace("\"", "").split(", "); 53 | Map authenticateFieldsMap = Arrays.stream(authenticateFields) 54 | .map(s -> s.split("=")) 55 | .collect(Collectors.toMap(a -> a[0], a -> a[1])); 56 | 57 | String realm = authenticateFieldsMap.get("realm"); 58 | if (realm == null) { 59 | throw new RuntimeException("Expected auth challenge to specify realm but it didn't"); 60 | } 61 | String serverNonce = authenticateFieldsMap.get("nonce"); 62 | if (serverNonce == null) { 63 | throw new RuntimeException("Expected auth challenge to specify nonce but it didn't"); 64 | } 65 | String qop = authenticateFieldsMap.get("qop"); 66 | if (qop == null || !Arrays.asList(qop.split(",")).contains("auth")) { 67 | throw new RuntimeException("Expected auth challenge to allow qop=auth but it didn't"); 68 | } 69 | 70 | return getAuthorizationHeaderValue(realm, serverNonce); 71 | } 72 | 73 | private String getAuthorizationHeaderValue(String realm, String serverNonce) { 74 | String qop = "auth"; 75 | String nonceCount = "00000001"; 76 | String clientNonce = randomNonce(); 77 | String ha1 = md5(username, realm, password); 78 | String ha2 = md5(requestMethod, requestPath); 79 | String responseVal = md5(ha1, serverNonce, nonceCount, clientNonce, qop, ha2); 80 | 81 | return "Digest " + 82 | "username=\"" + username + "\"," + 83 | "realm=\"" + realm + "\"," + 84 | "nonce=\"" + serverNonce + "\"," + 85 | "uri=\"" + requestPath + "\"," + 86 | "qop=auth," + 87 | "nc=" + nonceCount + "," + 88 | "cnonce=\"" + clientNonce + "\"," + 89 | "response=\"" + responseVal + "\""; 90 | } 91 | 92 | private String randomNonce() { 93 | byte[] nonceBytes = new byte[16]; 94 | random.nextBytes(nonceBytes); 95 | return Base64.getEncoder().encodeToString(nonceBytes); 96 | } 97 | 98 | private String md5(String... values) { 99 | return printHexBinary(messageDigest.digest(String.join(":", values).getBytes(UTF_8))).toLowerCase(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/rr/hikvisiondownloadassistant/IsapiRestClient.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Ryan Richard 2 | 3 | package rr.hikvisiondownloadassistant; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.dataformat.xml.XmlMapper; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | import rr.hikvisiondownloadassistant.Model.CMSearchDescription; 10 | import rr.hikvisiondownloadassistant.Model.CMSearchResult; 11 | import rr.hikvisiondownloadassistant.Model.SearchMatchItem; 12 | import rr.hikvisiondownloadassistant.Model.TimeSpan; 13 | 14 | import java.io.IOException; 15 | import java.net.URI; 16 | import java.net.http.HttpClient; 17 | import java.net.http.HttpRequest; 18 | import java.net.http.HttpResponse; 19 | import java.util.Date; 20 | import java.util.LinkedList; 21 | import java.util.List; 22 | 23 | import static rr.hikvisiondownloadassistant.DateConverter.dateToApiString; 24 | import static rr.hikvisiondownloadassistant.Model.PHOTOS_TRACK_ID; 25 | import static rr.hikvisiondownloadassistant.Model.VIDEOS_TRACK_ID; 26 | 27 | @Getter 28 | @RequiredArgsConstructor 29 | public class IsapiRestClient { 30 | 31 | private static final String GET = "GET"; 32 | private static final String POST = "POST"; 33 | private static final XmlMapper xmlMapper = new XmlMapper(); 34 | 35 | private final String host; 36 | private final String username; 37 | private final String password; 38 | 39 | public List searchVideos(Date fromDate, Date toDate) throws IOException, InterruptedException { 40 | return searchMedia(fromDate, toDate, VIDEOS_TRACK_ID); 41 | } 42 | 43 | public List searchPhotos(Date fromDate, Date toDate) throws IOException, InterruptedException { 44 | return searchMedia(fromDate, toDate, PHOTOS_TRACK_ID); 45 | } 46 | 47 | private List searchMedia(Date fromDate, Date toDate, int trackId) throws IOException, InterruptedException { 48 | List allResults = new LinkedList<>(); 49 | CMSearchResult searchResult; 50 | final int maxResults = 50; 51 | int searchResultPosition = 0; 52 | 53 | do { 54 | searchResult = doHttpRequest( 55 | POST, 56 | "/ISAPI/ContentMgmt/search", 57 | getSearchRequestBodyXml(fromDate, toDate, trackId, searchResultPosition, maxResults), 58 | CMSearchResult.class 59 | ); 60 | 61 | List matches = searchResult.getMatchList(); 62 | if (matches != null) { 63 | allResults.addAll(matches); 64 | } 65 | searchResultPosition += maxResults; 66 | 67 | } while (searchResult.isResponseStatus() && searchResult.getResponseStatusStrg().equalsIgnoreCase("more")); 68 | 69 | return allResults; 70 | } 71 | 72 | private String getSearchRequestBodyXml(Date fromDate, Date toDate, int trackId, int searchResultPosition, int maxResults) throws JsonProcessingException { 73 | return xmlMapper.writerWithDefaultPrettyPrinter().writeValueAsString( 74 | CMSearchDescription.builder() 75 | .maxResults(maxResults) 76 | .searchResultPosition(searchResultPosition) 77 | .timeSpan(List.of(TimeSpan.builder() 78 | .startTime(dateToApiString(fromDate)) 79 | .endTime(dateToApiString(toDate)) 80 | .build())) 81 | .trackID(List.of(trackId)) 82 | .build() 83 | ); 84 | } 85 | 86 | private T doHttpRequest(String requestMethod, String requestPath, String body, Class resultClass) throws IOException, InterruptedException { 87 | // Make the first request without an authorization header so we can get the digest challenge response. 88 | // See https://tools.ietf.org/html/rfc2617 89 | HttpResponse unauthorizedResponse = doHttpRequestWithAuthHeader(requestMethod, requestPath, body, null); 90 | if (unauthorizedResponse.statusCode() != 401) { 91 | throw new RuntimeException("Expected to get a 401 digest auth challenge response but didn't"); 92 | } 93 | 94 | // Calculate the authorization digest value 95 | String authorizationHeaderValue = new DigestAuth(unauthorizedResponse.headers(), requestMethod, requestPath, username, password) 96 | .getAuthorizationHeaderValue(); 97 | 98 | // Resend the request 99 | HttpResponse response = doHttpRequestWithAuthHeader(requestMethod, requestPath, body, authorizationHeaderValue); 100 | if (response.statusCode() == 401) { 101 | throw new RuntimeException("Could not authenticate. Wrong username or password?"); 102 | } 103 | if (response.statusCode() != 200) { 104 | throw new RuntimeException("Expected to get successful response but got response code " + response.statusCode()); 105 | } 106 | 107 | // Avoid a jackson parsing error where it doesn't like empty lists 108 | String workaroundForEmptyResult = response.body().replaceAll("\\s+", ""); 109 | 110 | // Return the parsed response 111 | return xmlMapper.readValue(workaroundForEmptyResult, resultClass); 112 | } 113 | 114 | private HttpResponse doHttpRequestWithAuthHeader(String requestMethod, String path, String body, String authHeaderValue) throws IOException, InterruptedException { 115 | HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() 116 | .uri(URI.create("http://" + host + path)) 117 | .header("Accept", "application/xml"); 118 | 119 | if (requestMethod.equals(POST)) { 120 | requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body)); 121 | } 122 | 123 | if (requestMethod.equals(GET)) { 124 | requestBuilder.GET(); 125 | } 126 | 127 | if (authHeaderValue != null) { 128 | requestBuilder.header("Authorization", authHeaderValue); 129 | } 130 | 131 | HttpRequest request = requestBuilder.build(); 132 | 133 | // System.err.println("Request Method: " + requestMethod); 134 | // System.err.println("Request URI: " + request.uri()); 135 | // System.err.println("Request Headers: " + request.headers().map()); 136 | // System.err.println("Request Body:\n" + body); 137 | 138 | HttpResponse response = HttpClient.newHttpClient() 139 | .send(request, HttpResponse.BodyHandlers.ofString()); 140 | 141 | // System.err.println("Response Code: " + response.statusCode()); 142 | // System.err.println("Response Headers: " + response.headers().map()); 143 | // System.err.println("Response Body:\n" + response.body()); 144 | 145 | return response; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/rr/hikvisiondownloadassistant/Main.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Ryan Richard 2 | 3 | package rr.hikvisiondownloadassistant; 4 | 5 | import picocli.CommandLine; 6 | import rr.hikvisiondownloadassistant.Model.SearchMatchItem; 7 | 8 | import java.io.IOException; 9 | import java.util.Date; 10 | import java.util.List; 11 | import java.util.logging.LogManager; 12 | 13 | import static rr.hikvisiondownloadassistant.DateConverter.dateToLocalHumanString; 14 | 15 | class Main { 16 | 17 | public static void main(String[] commandLineArguments) { 18 | LogManager.getLogManager().reset(); // disable all logging for now, since natty logs a bunch 19 | 20 | try { 21 | run(commandLineArguments); 22 | } catch (Exception e) { 23 | System.err.println("ERROR: " + e.getMessage()); 24 | System.exit(1); 25 | } 26 | } 27 | 28 | private static void run(String[] commandLineArguments) throws IOException, InterruptedException { 29 | Options options = parseCommandLineArguments(commandLineArguments); 30 | 31 | IsapiRestClient restClient = new IsapiRestClient(options.getHost(), options.getUsername(), options.getPassword()); 32 | Date fromDate = options.getFromDate(); 33 | Date toDate = options.getToDate(); 34 | 35 | if (!options.isQuiet()) { 36 | System.err.println("Getting photos and videos from \"" + 37 | dateToLocalHumanString(fromDate) + 38 | "\" to \"" + 39 | dateToLocalHumanString(toDate) + "\"\n"); 40 | } 41 | 42 | List videos = restClient.searchVideos(fromDate, toDate); 43 | List photos = restClient.searchPhotos(fromDate, toDate); 44 | 45 | if (photos.isEmpty() && videos.isEmpty() && !options.isQuiet()) { 46 | System.err.println("No photos or videos within that time/date range found"); 47 | return; 48 | } 49 | 50 | new OutputFormatter(options, videos, photos).printResults(); 51 | 52 | if (!options.isQuiet()) { 53 | System.err.println("\nFound " + videos.size() + " videos and " + photos.size() + " photos"); 54 | } 55 | 56 | } 57 | 58 | private static Options parseCommandLineArguments(String[] commandLineArguments) { 59 | Options options = new Options(); 60 | CommandLine commandLine = new CommandLine(options); 61 | commandLine.parseArgs(commandLineArguments); 62 | 63 | if (commandLine.isUsageHelpRequested()) { 64 | commandLine.usage(System.out); 65 | System.exit(0); 66 | } else if (commandLine.isVersionHelpRequested()) { 67 | commandLine.printVersionHelp(System.out); 68 | System.exit(0); 69 | } 70 | 71 | return options; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/rr/hikvisiondownloadassistant/Model.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Ryan Richard 2 | 3 | package rr.hikvisiondownloadassistant; 4 | 5 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.util.List; 12 | 13 | public class Model { 14 | 15 | public static final int VIDEOS_TRACK_ID = 101; 16 | public static final int PHOTOS_TRACK_ID = 103; 17 | 18 | @Getter 19 | @Builder 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public static class CMSearchDescription { 23 | @Builder.Default 24 | private String searchID = "search"; // supposed to be a guid in format ISO 9834-8, e.g. XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX numbers and capital letters 25 | 26 | private int maxResults; 27 | 28 | private int searchResultPosition; 29 | 30 | @JacksonXmlElementWrapper(localName = "trackIDList") 31 | private List trackID; // 101 is the main stream (videos), 102 is substream, 103 is the third stream (photos) 32 | 33 | // @JacksonXmlElementWrapper(localName = "contentTypeList") 34 | // private List contentType; // video, audio, metadata (for photos), text, mixed, other 35 | 36 | // private Metadata metadataList; // "//metadata.psia.org/VideoMotion" or when searching photos "//recordType.meta.std-cgi.com/MOTION" 37 | 38 | @JacksonXmlElementWrapper(localName = "timeSpanList") 39 | private List timeSpan; 40 | } 41 | 42 | @Getter 43 | @NoArgsConstructor 44 | public static class CMSearchResult { 45 | private String version; // e.g. 2.0 46 | private String searchID; 47 | private boolean responseStatus; 48 | private String responseStatusStrg; // e.g. OK, e.g. MORE (when paginating), e.g. NO MATCHES (for empty result) 49 | private long numOfMatches; 50 | private List matchList; 51 | } 52 | 53 | @Getter 54 | @NoArgsConstructor 55 | public static class SearchMatchItem { 56 | private String sourceID; // e.g. {0000000000-0000-0000-0000-000000000000} 57 | private int trackID; // e.g. 101 58 | private TimeSpan timeSpan; 59 | private MediaSegmentDescriptor mediaSegmentDescriptor; 60 | private Metadata metadataMatches; 61 | } 62 | 63 | @Getter 64 | @Builder 65 | @NoArgsConstructor 66 | @AllArgsConstructor 67 | public static class TimeSpan { 68 | private String startTime; // e.g. 2020-05-29T04:57:49Z 69 | private String endTime; // e.g. 2020-05-29T04:58:05Z 70 | } 71 | 72 | @Getter 73 | @NoArgsConstructor 74 | public static class MediaSegmentDescriptor { 75 | private String contentType; // e.g. video or picture 76 | 77 | private String codecType; // e.g. H.264-BP or jpeg 78 | 79 | // e.g. rtsp://192.168.1.64/Streaming/tracks/101/?starttime=20200529T045749Z&endtime=20200529T045805Z&name=ch01_00000000000000613&size=2901372 80 | // e.g. http://192.168.1.64/ISAPI/Streaming/tracks/103/?starttime=20200531T012016Z&endtime=20200531T012016Z&name=ch01_00000000001026401&size=600489 81 | private String playbackURI; 82 | } 83 | 84 | @Getter 85 | @Builder 86 | @NoArgsConstructor 87 | @AllArgsConstructor 88 | public static class Metadata { 89 | private String metadataDescriptor; // e.g. recordType.meta.hikvision.com/AllEvent 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/rr/hikvisiondownloadassistant/Options.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Ryan Richard 2 | 3 | package rr.hikvisiondownloadassistant; 4 | 5 | import com.joestelmach.natty.DateGroup; 6 | import com.joestelmach.natty.Parser; 7 | import lombok.Getter; 8 | import picocli.CommandLine.Command; 9 | import picocli.CommandLine.Option; 10 | import picocli.CommandLine.Parameters; 11 | 12 | import java.util.Date; 13 | import java.util.List; 14 | 15 | import static lombok.AccessLevel.PRIVATE; 16 | 17 | @Getter 18 | @Command( 19 | name = "java -jar hikvision-download-assistant.jar", 20 | version = "1.1.0", 21 | mixinStandardHelpOptions = true, 22 | usageHelpAutoWidth = true 23 | ) 24 | public class Options { 25 | 26 | @Parameters( 27 | paramLabel = "HOST", 28 | description = "Connect to this host or IP address to perform search." 29 | ) 30 | private String host; 31 | 32 | @Parameters( 33 | paramLabel = "USERNAME", 34 | description = "Use this username when connecting to perform search." 35 | ) 36 | private String username; 37 | 38 | @Parameters( 39 | paramLabel = "PASSWORD", 40 | description = "Use this password when connecting to perform search." 41 | ) 42 | private String password; 43 | 44 | @Option( 45 | names = {"-f", "--from-time"}, 46 | defaultValue = "24 hours ago", 47 | description = "Search starting from this time, entered using English natural language. Defaults to '${DEFAULT-VALUE}'." 48 | ) 49 | @Getter(value = PRIVATE) 50 | private String fromTime; 51 | 52 | @Option( 53 | names = {"-t", "--to-time"}, 54 | defaultValue = "now", 55 | description = "Search up to this time, entered using English natural language. Defaults to '${DEFAULT-VALUE}'." 56 | ) 57 | @Getter(value = PRIVATE) 58 | private String toTime; 59 | 60 | @Option( 61 | names = {"-p", "--output-password"}, 62 | description = "Output a different password in the printed curl commands, e.g. '$PASSWORD'." 63 | ) 64 | private String outputPassword; 65 | 66 | @Option( 67 | names = {"-u", "--output-username"}, 68 | description = "Output a different username in the printed curl commands, e.g. '$USERNAME'." 69 | ) 70 | private String outputUsername; 71 | 72 | @Option( 73 | names = {"-d", "--table-delimiter"}, 74 | defaultValue = "|", 75 | description = "The column delimiter for table output. Defaults to '${DEFAULT-VALUE}'." 76 | ) 77 | private String tableDelimiter; 78 | 79 | @Option( 80 | names = {"-q", "--quiet"}, 81 | description = "Suppress header and footer." 82 | ) 83 | private boolean quiet; 84 | 85 | enum OutputFormat { 86 | json, 87 | table 88 | } 89 | 90 | @Option( 91 | names = {"-o", "--output"}, 92 | defaultValue = "table", 93 | description = "Output format. Can be 'table' or 'json'. Defaults to '${DEFAULT-VALUE}'." 94 | ) 95 | private OutputFormat outputFormat; 96 | 97 | public Date getFromDate() { 98 | return getDateFromNaturalLanguage(fromTime); 99 | } 100 | 101 | public Date getToDate() { 102 | return getDateFromNaturalLanguage(toTime); 103 | } 104 | 105 | private Date getDateFromNaturalLanguage(String naturalLanguageTimeDescription) { 106 | Parser parser = new Parser(); 107 | List groups = parser.parse(naturalLanguageTimeDescription); 108 | if (groups.size() != 1) { 109 | throw new RuntimeException("Please describe one date/time for a time option"); 110 | } 111 | DateGroup dateGroup = groups.get(0); 112 | if (dateGroup.isRecurring()) { 113 | throw new RuntimeException("Please do not use recurring date/times"); 114 | } 115 | if (dateGroup.getDates().size() != 1) { 116 | throw new RuntimeException("Please describe one date/time for a time option"); 117 | } 118 | return dateGroup.getDates().get(0); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/rr/hikvisiondownloadassistant/OutputFormatter.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Ryan Richard 2 | 3 | package rr.hikvisiondownloadassistant; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | import rr.hikvisiondownloadassistant.Model.SearchMatchItem; 11 | 12 | import java.util.Comparator; 13 | import java.util.Date; 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | 17 | import static lombok.AccessLevel.PRIVATE; 18 | import static rr.hikvisiondownloadassistant.DateConverter.*; 19 | 20 | @RequiredArgsConstructor 21 | public class OutputFormatter { 22 | 23 | private enum MediaType { 24 | PHOTO, 25 | VIDEO 26 | } 27 | 28 | @Builder 29 | @Getter 30 | private static class Metadata { 31 | private final String host; 32 | private final String fromHumanReadableTime; 33 | private final String toHumanReadableTime; 34 | private final String fromTime; 35 | private final String toTime; 36 | } 37 | 38 | @Builder 39 | @Getter 40 | private static class JsonOutput { 41 | private final Metadata metadata; 42 | private final List results; 43 | } 44 | 45 | private final Options options; 46 | private final List videos; 47 | private final List photos; 48 | 49 | public void printResults() { 50 | List rows = convertToOutputRows(MediaType.VIDEO, videos); 51 | rows.addAll(convertToOutputRows(MediaType.PHOTO, photos)); 52 | 53 | rows.sort(Comparator.comparing(OutputRow::getStartTime)); 54 | 55 | if (options.getOutputFormat().equals(Options.OutputFormat.table)) { 56 | printTableOutput(rows); 57 | } else { 58 | printJsonOutput(rows); 59 | } 60 | } 61 | 62 | private void printJsonOutput(List rows) { 63 | JsonOutput jsonOutput = JsonOutput.builder() 64 | .results(rows) 65 | .metadata(Metadata.builder() 66 | .host(options.getHost()) 67 | .fromHumanReadableTime(dateToLocalHumanString(options.getFromDate())) 68 | .toHumanReadableTime(dateToLocalHumanString(options.getToDate())) 69 | .fromTime(dateToLocalFilenameString(options.getFromDate())) 70 | .toTime(dateToLocalFilenameString(options.getToDate())) 71 | .build()) 72 | .build(); 73 | ObjectMapper objectMapper = new ObjectMapper(); 74 | try { 75 | System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput)); 76 | } catch (JsonProcessingException e) { 77 | throw new RuntimeException(e); 78 | } 79 | } 80 | 81 | private void printTableOutput(List rows) { 82 | String tableColumnDelimiter = options.getTableDelimiter(); 83 | 84 | if (!options.isQuiet()) { 85 | String headers = String.join(tableColumnDelimiter, List.of("Type", "EventType", "Start", "End", "Curl")); 86 | String underline = new String(new char[headers.length()]).replace("\0", "-"); 87 | System.err.println(headers); 88 | System.err.println(underline); 89 | } 90 | 91 | rows.stream() 92 | .map(OutputRow::toTextTableRow) 93 | .forEach(row -> System.out.println(String.join(tableColumnDelimiter, row))); 94 | } 95 | 96 | private List convertToOutputRows(MediaType mediaType, List items) { 97 | return items.stream().map(item -> new OutputRow( 98 | item, 99 | mediaType, 100 | apiStringToDate(item.getTimeSpan().getStartTime()), 101 | apiStringToDate(item.getTimeSpan().getEndTime()) 102 | ) 103 | ).collect(Collectors.toList()); 104 | } 105 | 106 | @Getter 107 | @RequiredArgsConstructor 108 | private class OutputRow { 109 | 110 | @Getter(value = PRIVATE) 111 | private final SearchMatchItem item; 112 | 113 | private final MediaType mediaType; 114 | private final Date startTime; 115 | private final Date endTime; 116 | 117 | public List toTextTableRow() { 118 | return List.of( 119 | mediaType.toString(), 120 | getEventType(), 121 | dateToLocalString(startTime), 122 | dateToLocalString(endTime), 123 | getCurlCommand() 124 | ); 125 | } 126 | 127 | public String getCurlCommand() { 128 | return mediaType == MediaType.PHOTO ? formatPhotoCurlCommand() : formatVideoCurlCommand(); 129 | } 130 | 131 | public String getEventType() { 132 | return item.getMetadataMatches() 133 | .getMetadataDescriptor() 134 | .replace("recordType.meta.hikvision.com/", "") 135 | .toUpperCase(); 136 | } 137 | 138 | private String getPlaybackURI() { 139 | return item.getMediaSegmentDescriptor().getPlaybackURI(); 140 | } 141 | 142 | private String formatVideoCurlCommand() { 143 | return String.join(" ", List.of( 144 | "curl", 145 | "-f", 146 | "--anyauth --user " + getOutputUsername() + ":" + getOutputPassword(), 147 | "-X GET", 148 | "-d '" + getPlaybackURI().replace("&", "&") + "'", 149 | "'http://" + options.getHost() + "/ISAPI/ContentMgmt/download'", 150 | "--output " + dateToLocalFilenameString(startTime) + ".mp4" 151 | )); 152 | } 153 | 154 | private String formatPhotoCurlCommand() { 155 | return String.join(" ", List.of( 156 | "curl", 157 | "-f", 158 | "--anyauth --user " + getOutputUsername() + ":" + getOutputPassword(), 159 | "'" + getPlaybackURI() + "'", 160 | "--output " + dateToLocalFilenameString(startTime) + "." + item.getMediaSegmentDescriptor().getCodecType() 161 | )); 162 | } 163 | 164 | private String getOutputUsername() { 165 | return options.getOutputUsername() == null ? options.getUsername() : options.getOutputUsername(); 166 | } 167 | 168 | private String getOutputPassword() { 169 | return options.getOutputPassword() == null ? options.getPassword() : options.getOutputPassword(); 170 | } 171 | 172 | } 173 | 174 | } 175 | --------------------------------------------------------------------------------