├── .gitignore ├── log └── nuke-logs ├── config.streamlink ├── README.md ├── encoder ├── scheduler ├── config.example └── listener /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*.* 3 | !*/ 4 | !listener 5 | !encoder 6 | !scheduler 7 | !/log/nuke-logs 8 | config 9 | /thumbs 10 | /log/*.log 11 | /log/*.log.old 12 | /src/* 13 | /raw-src/* 14 | /out/* 15 | -------------------------------------------------------------------------------- /log/nuke-logs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source config 3 | 4 | 5 | cd $LOGDIR; 6 | find . -type f \( -name "*.log" -not -name "ffmpeg.log" -not -name "ffmpeg.log.old" \) -print0 | xargs -I{} -0 rm {}; 7 | 8 | for file in ffmpeg.log.old; do 9 | tail -200000 "$file" > "$file.tmp" # retain 200k lines of logs in ffmpeg.old 10 | mv -- "$file.tmp" "$file" 11 | done 12 | 13 | echo "nuked" > ffmpeg.log; 14 | printf "\n\n${BLUE}logs nuked!${NC}\n" 15 | ls -lah $LOGDIR 16 | -------------------------------------------------------------------------------- /config.streamlink: -------------------------------------------------------------------------------- 1 | ### Documentation: 2 | ### https://streamlink.github.io/cli.html#configuration-file 3 | 4 | retry-streams 5 5 | # ^ Delay, in seconds, between each attempt to fetch a stream 6 | 7 | retry-max 0 8 | # ^ How many attempts before giving up on fetching? (0 = retry infinitely) 9 | 10 | default-stream=best,720p,480p 11 | # ^ Try for best quality, then 720p, then 480p 12 | 13 | # Comment out the line below to allow quality below 480p 14 | stream-sorting-excludes=<480p 15 | 16 | ### https://streamlink.github.io/cli.html#stream-transport-options 17 | stream-segment-threads=3 18 | stream-segment-attempts=3 19 | stream-segment-timeout=10 20 | stream-timeout=2 21 | http-timeout=10 22 | # ^ optimizing segment parameters (feel free to comment out and go with defaults) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SGAT - StreamGrab And Transcode [1.1.2] 2 | 3 | >A wrapper for ffmpeg and streamlink to grab your favorite streams live and encode them right after 4 | 5 | **Note:** a more thorough documentation is scheduled for **soon™** 6 | ### If you ran SGAT 1.1.1 and below, **please update your config!** 7 | 8 | ## Installation 9 | 10 | ```bash 11 | git clone https://github.com/TildeSlashC0re/stream-grab-and-transcode.git; 12 | cd stream-grab-and-transcode; 13 | cat config.example > config; 14 | nano config # Provide, at minimum(!), $WORKDIR 15 | 16 | # Optionally you can also 17 | nano config.streamlink 18 | ``` 19 | 20 | ## Dependencies 21 | 22 | ```bash 23 | sudo apt-get update; 24 | sudo apt-get install python-pip ffmpeg; 25 | pip install streamlink 26 | ``` 27 | 28 | ### Optional Dependencies 29 | ```bash 30 | # if you defined $APPRISE_HOOK_TARGET in your config you need to 31 | pip install apprise 32 | ``` 33 | 34 | ## Running 35 | 36 | ```bash 37 | ./listener 38 | 39 | # OR 40 | ./listener 41 | ``` 42 | 43 | ## Exiting 44 | 45 | ```bash 46 | CTRL + C 47 | ``` 48 | 49 | ## Cleaning Up Log Directory 50 | 51 | ```bash 52 | ./log/nuke-logs 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /encoder: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source config 3 | 4 | mkdir -p out/$srcstring 5 | mkdir -p raw-src/$srcstring 6 | 7 | cd $WORKDIR/src/$srcstring; 8 | shopt -s nullglob 9 | 10 | notify_output(){ 11 | cat $LOGDIR/ffmpeg.log | 12 | awk '!seen[$0]++' | 13 | sed 's/\x1b\[[0-9;]*m//g' | 14 | sed -n '/Metadata/,$p' | 15 | sed -n '/frame.*/q;p' | 16 | head -c 1900 | 17 | sed -e "1s/^/\`\`\`arm\\n/" -e "\$s/\$/\\n\`\`\`/" | 18 | apprise "$APPRISE_HOOK_TARGET"; 19 | } 20 | 21 | notify() 22 | { 23 | if [ ! -z "$APPRISE_HOOK_TARGET" ]; then 24 | if grep -E -q 'Input' $LOGDIR/ffmpeg.log 25 | then 26 | cat $LOGDIR/ffmpeg.log | 27 | awk '!seen[$0]++' | 28 | sed 's/\x1b\[[0-9;]*m//g' | 29 | sed -n '/Metadata.*/q;p' | 30 | head -c 1900 | 31 | sed -e "1s/^/\`\`\`arm\\n/" -e "\$s/\$/\\n\`\`\`/" | 32 | sed -e "1s/^/\\nEncoder\ flags\:\\n\`$ENCODER_FLAGS\`\\n\\n/" | 33 | apprise -t "$NOTIFY_JOB_RUNNING" "$APPRISE_HOOK_TARGET"; 34 | notify_output 35 | else 36 | true 37 | fi 38 | else 39 | true 40 | fi 41 | } 42 | 43 | sendjob() 44 | { 45 | for FILE in *.mp4; do 46 | [ -f "$FILE" ] || continue 47 | echo "ffmpeg -y -i $WORKDIR/src/$srcstring/$FILE -hide_banner $ENCODER_FLAGS -f mp4 $WORKDIR/out/$srcstring/$FILE > $LOGDIR/ffmpeg.log 2>&1" > $WORKDIR/ffmpeg-queue 48 | sleep 1s; 49 | notify 50 | mv $FILE $WORKDIR/raw-src/$srcstring/ 51 | done 52 | } 53 | 54 | sendjob 2> /dev/null; 55 | cd $WORKDIR 56 | -------------------------------------------------------------------------------- /scheduler: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source config 3 | 4 | notify() 5 | { 6 | if [ ! -z "$APPRISE_HOOK_TARGET" ]; then 7 | head -c 1950 | 8 | ls -qsh1 out/$srcstring raw-src/$srcstring | 9 | sed 's/\x1b\[[0-9;]*m//g' | 10 | sed -e "1s/^/\`\`\`nginx\\n/" -e "\$s/\$/\`\`\`/" | 11 | apprise -t "$NOTIFY_ENCODE_FINISHED" "$APPRISE_HOOK_TARGET"; 12 | else 13 | true 14 | fi 15 | } 16 | 17 | jobscomplete() 18 | { 19 | echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" 20 | printf -- "- - - - - - - - - - - - ${HIGHLIGHT}J O B F I N I S H E D${NC} - - - - - - - - - - \n" 21 | echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" 22 | printf "\n\ncheck ${HIGHLIGHT}out/$srcstring${NC} for the goodies!\n\n" >> $LOGDIR/ffmpeg.log 23 | echo "waiting for new job. . ." >> $LOGDIR/ffmpeg.log; 24 | } 25 | 26 | logrotate () 27 | { 28 | cat $LOGDIR/ffmpeg.log >> $LOGDIR/ffmpeg.log.old; 29 | printf "\nsee ${HIGHLIGHT}$LOGDIR/ffmpeg.log.old${NC} for completed jobs!\n\n" > $LOGDIR/ffmpeg.log 30 | } 31 | 32 | 33 | 34 | pipe=$WORKDIR/ffmpeg-queue 35 | trap "rm -f $pipe;" EXIT 36 | 37 | logrotate; 38 | 39 | # creating the FIFO 40 | [[ -p $pipe ]] || mkfifo $pipe 41 | 42 | while true; do 43 | # can't just use "while read line" if we 44 | # want this script to continue running. 45 | read line < $pipe 46 | 47 | # now implementing a bit of security, 48 | # feel free to improve it. 49 | # we ensure that the command is a ffmpeg one. 50 | [[ $line =~ ^ffmpeg ]] && bash <<< "$line" 51 | logrotate; 52 | notify; 53 | jobscomplete >> $LOGDIR/ffmpeg.log 54 | done 55 | -------------------------------------------------------------------------------- /config.example: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # provide absolute path to the root of this git repository 5 | # 6 | # e.g. 7 | # WORKDIR=/home/c0re/Downloads/stream-grab-and-transcode 8 | # 9 | # !!!!!!!!!!!!!!!!!!!!!!! 10 | # !! NO TRAILING SLASH !! 11 | # !!!!!!!!!!!!!!!!!!!!!!! 12 | # 13 | WORKDIR="" 14 | LOGDIR=$WORKDIR"/log" 15 | 16 | # 17 | # target to grab e.g. https://stream.me/collapz 18 | # if left empty, you will be prompted on each run. 19 | # 20 | # STREAM="https://www.youtube.com/watch?v=___________" 21 | # Do not use shortened (= youtu.be/*) links for YouTube targets! 22 | STREAM="" 23 | 24 | # 25 | # the player of your choice 26 | # (for playback during live recording) 27 | # leave empty for headless mode / server environment 28 | PLAYER="" 29 | 30 | # 31 | # pass options to your player 32 | # 33 | PLAYER_OPTIONS="" 34 | 35 | # 36 | # ffmpeg encoding flags. 37 | # You may adjust those according to your needs. 38 | # Partial Doc: https://trac.ffmpeg.org/wiki/Encode/H.264 39 | # 40 | ENCODER_FLAGS="-r 30 -c:v libx264 -preset veryfast -crf 23 -speed 4 -movflags +faststart -c:a aac -af \"aresample=first_pts=0\"" 41 | 42 | # 43 | # Notification-hooks via apprise: 44 | # See documentation: https://github.com/caronc/apprise/wiki 45 | # 46 | NOTIFY_STREAM_LIVE="[🔴] SGAT is grabbing your target!" 47 | NOTIFY_JOB_RUNNING="[⏳] SGAT is processing an encoding job!" 48 | NOTIFY_ENCODE_FINISHED="[✅] SGAT has finished an encoding job!" 49 | # 50 | # Example: APPRISE_HOOK_TARGET="discord:////?thumbnail=no&avatar=no tgram:///?format=markdown" 51 | # ^ sends notification to discord ^ sends notification to telegram 52 | APPRISE_HOOK_TARGET="" 53 | 54 | #---R E S T R E A M I N G---------------------------------------------------------------------------------------------------------------------------------- 55 | # 56 | # Use at your own discretion 57 | # it's pretty sweet though. 58 | # (just don't get yourself banned for stupid shit) 59 | 60 | RESTREAM="false" 61 | #RESTREAM_ENDPOINT="rtmp://host/endpoint" 62 | #RESTREAM_ENDPOINT_KEY="yourkey" 63 | #TRANSCODER_FLAGS="-c:v libx264 -x264-params nal-hrd=cbr -b:v 2000k -minrate 2000k -maxrate 2000k -bufsize 2000k -preset fast -tune film -x264-params keyint=60 -pix_fmt yuv420p -c:a aac" 64 | 65 | 66 | # === 67 | # Highlight Color - There is usually no need to change this. 68 | # So feel free to do it anyway ;) 69 | # 70 | # NC - No Color 71 | # === 72 | HIGHLIGHT='\033[0;34m' 73 | NC='\033[0m' 74 | 75 | -------------------------------------------------------------------------------- /listener: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source config 2>/dev/null 3 | ARG1=$1 4 | 5 | # Go to end-sequence on CTRL-C 6 | trap rollcredits SIGINT SIGTERM 7 | 8 | intro() { 9 | clear 10 | printf -- "$HIGHLIGHT" 11 | printf -- " .|'''.| ..|'''.| | |''||''| \n" 12 | printf -- " ||.. ' .|' ' ||| || \n" 13 | printf -- " ''|||. || .... | || || \n" 14 | printf -- " . '|| '|. || .''''|. || \n" 15 | printf -- " |'....|' ''|...'| .|. .||. .||. \n${NC}" 16 | printf -- " - - - - - - - -[ _ _ _ ] - - - - - - - -" 17 | sleep 2s; 18 | } 19 | 20 | ## 21 | ## Declare functions 22 | ## 23 | 24 | # Various messages 25 | lineprint() 26 | { 27 | printf -- "\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n" 28 | } 29 | 30 | saygoodbye() 31 | { 32 | sleep 1s; 33 | lineprint 34 | printf "${HIGHLIGHT}bye${NC}!" 35 | lineprint 36 | } 37 | 38 | # Inform user of script re-run 39 | loop_end_message() 40 | { 41 | sleep 2s; 42 | printf "${HIGHLIGHT}listening once more${NC} ☜(゚ヮ゚☜)!\n" 43 | echo "- - - - - - - - - - - - - - - - - - - - - -" 44 | echo "- - - - - - N E W J O B - - - - - - -" 45 | echo "- - - - - - - - - - - - - - - - - - - - - -" 46 | } 47 | 48 | # basic sanity checks ... config provided? WORKDIR defined? STREAM defined? 49 | basicsanity() 50 | { 51 | if [ ! -f ./config ]; then 52 | lineprint 53 | printf "${HIGHLIGHT}N O C O N F I G F O U N D${NC}! » » » S E E ${HIGHLIGHT}R E A D M E . m d ${NC}" 54 | lineprint 55 | exit 2; 56 | fi 57 | 58 | if [ -z "$WORKDIR" ]; then 59 | lineprint 60 | printf "P L E A S E ${HIGHLIGHT}D E F I N E W O R K D I R${NC} I N ${HIGHLIGHT}. / C O N F I G${NC}" 61 | lineprint 62 | exit 2; 63 | fi 64 | 65 | if [ -z "$STREAM" ]; then 66 | trap "saygoodbye; exit 0" SIGINT 67 | lineprint 68 | printf "${HIGHLIGHT}No \$STREAM${NC} in ${HIGHLIGHT}./config${NC} defined!\nPlease provide a stream to grab now:\n" 69 | read -p "" STREAM 70 | if [ -z "$STREAM" ]; then 71 | lineprint 72 | echo "No stream provided. Exiting!"; 73 | saygoodbye 74 | exit 1 75 | else 76 | if streamlink --can-handle-url $STREAM 2>/dev/null; then 77 | true 78 | else 79 | echo "Incompatible stream provided. Exiting!"; 80 | saygoodbye 81 | exit 1 82 | fi 83 | fi 84 | fi 85 | 86 | 87 | } 88 | 89 | restreamflag() { 90 | lineprint 91 | printf -- "\n- - - - - - - R E S T R E A M F L A G ${HIGHLIGHT}A C T I V E ${NC}! - - - - - -" 92 | } 93 | 94 | restreamkeycheck() { 95 | if [ -z "$RESTREAM_ENDPOINT_KEY" ]; then 96 | lineprint 97 | printf "Provide your ${HIGHLIGHT}keys${NC} please!${NC}\n" 98 | read -p "" RESTREAM_ENDPOINT_KEY 99 | if [ -z "$RESTREAM_ENDPOINT_KEY" ]; then 100 | printf "No ${HIGHLIGHT}keys${NC} no ${HIGHLIGHT}restream!${NC}\n" 101 | saygoodbye; exit 0 102 | else 103 | restreamflag 104 | fi 105 | else 106 | restreamflag 107 | fi 108 | } 109 | 110 | restreamcheck() { 111 | trap "saygoodbye; exit 0" SIGINT SIGTERM 112 | clear 113 | if [ ! "$RESTREAM" = "true" ]; then 114 | true 115 | else 116 | if [ -z "$RESTREAM_ENDPOINT" ]; then 117 | lineprint 118 | printf "No ${HIGHLIGHT}\$RESTREAM_ENDPOINT${NC} in ${HIGHLIGHT}./config${NC} defined! Please provide a ${HIGHLIGHT}rtmp://${NC} target to restream to now:\n" 119 | read -p "" RESTREAM_ENDPOINT 120 | if [ -z "$RESTREAM_ENDPOINT" ]; then 121 | lineprint 122 | echo "No endpoint provided, will continue without restreaming the source!" 123 | else 124 | restreamkeycheck 125 | fi 126 | else 127 | restreamkeycheck 128 | fi 129 | fi 130 | } 131 | 132 | restream() { 133 | streamlink --config config.streamlink $STREAM -O 2>> $LOGDIR/$DATE.log | tee $WORKDIR/src/$srcstring/$DATE.mp4 | transcode >> $LOGDIR/$DATE.log 2>&1; 134 | } 135 | 136 | transcode() { 137 | ffmpeg -hide_banner -re -i - $TRANSCODER_FLAGS -movflags frag_keyframe+empty_moov -f flv $RESTREAM_ENDPOINT/$RESTREAM_ENDPOINT_KEY 138 | } 139 | 140 | # Truncate $STREAM for something more handy. Define $srcstring. Use that as an identifier. 141 | contain-instance() { 142 | srcstring="${STREAM?need a string}" # receive input in first argument 143 | srcstring="${srcstring//[^[:alnum:]]/}" # replace all non-alnum characters to - 144 | srcstring="${srcstring//+(-)/-}" # convert multiple - to single - 145 | srcstring="${srcstring/#-}" # remove - from start 146 | srcstring="${srcstring/#https}" # remove "https" from start 147 | srcstring="${srcstring/comwatchv/_}" # YouTube underscore 148 | srcstring="${srcstring/streamme/streamme_}" # StreamMe underscore 149 | srcstring="${srcstring/#www}" # remove "www" from start 150 | srcstring="${srcstring/%-}" # remove - from end 151 | srcstring="${srcstring,,}" 152 | srcstring="$srcstring" 153 | mkdir -p src/$srcstring 154 | export srcstring=$srcstring 155 | } 156 | 157 | argument(){ 158 | if [ -z "$ARG1" ]; then 159 | true 160 | else 161 | if streamlink --can-handle-url $ARG1 2>/dev/null; then 162 | STREAM=$ARG1 163 | true 164 | else 165 | lineprint 166 | echo "Incompatible stream provided. Exiting!"; 167 | saygoodbye 168 | exit 1 169 | fi 170 | fi 171 | } 172 | 173 | # Remove 0byte files recursively inside WORKDIR, remove empty directories inside WORKDIR. 174 | cleanup_0byte() 175 | { 176 | find . -type f \( -not -name "ffmpeg.log" \) -size 0 -print0 | xargs -I{} -0 rm {}; 177 | find . -type d -empty -delete; 178 | } 179 | 180 | # Log output to console 181 | log_encoder() 182 | { 183 | lineprint 184 | printf "${HIGHLIGHT}ffmpeg${NC} log:" 185 | lineprint 186 | tail -F -n 10 $LOGDIR/ffmpeg.log 2>/dev/null; 187 | } 188 | 189 | log_listener() 190 | { 191 | touch $LOGDIR/$DATE.log 192 | lineprint 193 | printf "${HIGHLIGHT}streamlink${NC} log:" 194 | lineprint 195 | printf "\n" 196 | tail -F $LOGDIR/$DATE.log 2>/dev/null & 197 | } 198 | 199 | # End log output to console 200 | kill_log_encoder() 201 | { 202 | kill $(pgrep -f "^(tail -F.*$LOGDIR/ffmpeg.log).*$") 2>/dev/null 203 | } 204 | 205 | kill_log_listener() 206 | { 207 | kill $(pgrep -f "^(tail -F.*$LOGDIR/$DATE).*$") 2>/dev/null 208 | } 209 | 210 | 211 | # End-sequence 212 | rollcredits() 213 | { 214 | kill_log_listener; 215 | runjobs; 216 | log_encoder; 217 | menu; 218 | } 219 | 220 | # Exit screen 221 | menu() 222 | { 223 | clear 224 | trap menu SIGINT SIGTERM 225 | lineprint 226 | printf "Do you want to ${HIGHLIGHT}kill the scheduler${NC}?" 227 | lineprint 228 | printf "${HIGHLIGHT}[warning!]${NC} this will ${HIGHLIGHT}abort pending jobs${NC}!" 229 | lineprint 230 | kill_log_encoder; 231 | select yn in "Yes" "No" "Show Log"; do 232 | case $yn in 233 | Yes ) kill_log_encoder; killjobs; pkill scheduler; cleanup_0byte; saygoodbye; exit 0;; 234 | No ) cleanup_0byte; saygoodbye; exit 0;; 235 | Show\ Log ) log_encoder; menu;; 236 | esac 237 | done 238 | } 239 | 240 | # FFMPEG queue pipe 241 | runscheduler() 242 | { 243 | if ! pgrep -x "scheduler" > /dev/null 244 | then 245 | setsid nohup ./scheduler >/dev/null 2>&1 & 246 | printf "\n\n${HIGHLIGHT}ffmpeg-scheduler${NC} started ${HIGHLIGHT}in background.${NC}\n" 247 | else 248 | printf "\n\n${HIGHLIGHT}ffmpeg-scheduler${NC} already running.${NC}\n" 249 | fi 250 | } 251 | 252 | # Provide timestamp per job 253 | timestamp() 254 | { 255 | DATE="$(date +%m_%d_%Y-%H:%M:%S-%s)" 256 | printf "\n\nwill write temporary output to: \n\nsrc/$srcstring/${HIGHLIGHT}"$DATE"${NC}.mp4\n"; 257 | if [ ! "$RESTREAM" = "true" ]; then 258 | true 259 | else 260 | lineprint 261 | printf "\nRestreaming to: ${HIGHLIGHT}"$RESTREAM_ENDPOINT"${NC}\n"; 262 | fi 263 | } 264 | 265 | # Streamlink 266 | streamlink_listen() 267 | { 268 | if [ -z "$PLAYER" ]; 269 | then 270 | lineprint 271 | printf -- "- - - - - R U N N I N G I N ${HIGHLIGHT}H E A D L E S S M O D E${NC} - - - - -" 272 | lineprint 273 | 274 | streamlink --config config.streamlink $STREAM -o $WORKDIR/src/$srcstring/$DATE.mp4 > $LOGDIR/$DATE.log 2>&1; 275 | else 276 | streamlink --config config.streamlink $STREAM -O 2>> $LOGDIR/$DATE.log | tee $WORKDIR/src/$srcstring/$DATE.mp4 | $PLAYER - $PLAYER_OPTIONS >> $LOGDIR/$DATE.log 2>&1; 277 | fi 278 | } 279 | 280 | # Send jobs to FFMPEG queue 281 | runjobs() 282 | { 283 | clear 284 | cleanup_0byte; 285 | setsid nohup ./encoder > /dev/null 2>&1 & 286 | lineprint 287 | printf "${HIGHLIGHT}scheduled job${NC} for src/$srcstring/${HIGHLIGHT}$DATE${NC}.mp4!" 288 | lineprint 289 | } 290 | 291 | notify() 292 | { 293 | if [ ! -z "$APPRISE_HOOK_TARGET" ]; then 294 | if grep -E -q 'Available' $LOGDIR/$DATE.log 295 | then 296 | cat $LOGDIR/$DATE.log | 297 | awk '!seen[$0]++' | 298 | sed 's/\x1b\[[0-9;]*m//g' | 299 | sed -e "1s/^/\`\`\`arm\\n/" -e "\$s/\$/\`\`\`/" | 300 | apprise -t "$NOTIFY_STREAM_LIVE" "$APPRISE_HOOK_TARGET"; 301 | else 302 | sleep 2s; 303 | notify; 304 | fi 305 | else 306 | true 307 | fi 308 | } 309 | 310 | # Kill a running FFMPEG job 311 | killjobs() 312 | { 313 | kill $(pgrep -f "^(ffmpeg -y -i.*.mp4.-hide_banner).*$") >/dev/null 2>&1 & 314 | } 315 | 316 | # That main-function really tied the room together. 317 | main() 318 | { 319 | trap rollcredits SIGINT SIGTERM 320 | while true; do 321 | cd $WORKDIR 322 | timestamp; 323 | log_listener; 324 | if [ "$RESTREAM" = "true" ]; then 325 | restream 326 | else 327 | notify & 328 | streamlink_listen 329 | fi 330 | runjobs; 331 | loop_end_message; 332 | done 333 | } 334 | 335 | ## 336 | ## Roll the script! 337 | ## 338 | intro; 339 | basicsanity; 340 | argument; 341 | contain-instance; 342 | runscheduler; 343 | restreamcheck; 344 | main 345 | 346 | --------------------------------------------------------------------------------